blob: 51ce13954574f8edcd524f0a236f182eceaaaf88 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.tests.operations;
import junit.framework.TestCase;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.DefaultOperationHistory;
import org.eclipse.core.commands.operations.ICompositeOperation;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.IOperationApprover;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.LinearUndoEnforcer;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.commands.operations.OperationHistoryFactory;
import org.eclipse.core.commands.operations.OperationStatus;
import org.eclipse.core.commands.operations.TriggeredOperations;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
/**
* Tests the Operations Framework API.
*
* @since 3.1
*/
public class OperationsAPITest extends TestCase {
ObjectUndoContext contextA, contextB, contextC, contextW;
IOperationHistory history;
IUndoableOperation op1, op2, op3, op4, op5, op6, localA, localB, localC;
ICompositeOperation refactor;
int preExec, postExec, preUndo, postUndo, preRedo, postRedo, add, remove, notOK, changed = 0;
IOperationHistoryListener listener;
public OperationsAPITest() {
super();
}
/**
* @param testName
*/
public OperationsAPITest(String name) {
super(name);
}
protected void setUp() throws Exception {
history = new DefaultOperationHistory();
contextA = new ObjectUndoContext("A");
contextB = new ObjectUndoContext("B");
contextC = new ObjectUndoContext("C");
op1 = new TestOperation("op1");
op1.addContext(contextA);
op2 = new TestOperation("op2");
op2.addContext(contextB);
op2.addContext(contextC);
op3 = new TestOperation("op3");
op3.addContext(contextC);
op4 = new TestOperation("op4");
op4.addContext(contextA);
op5 = new TestOperation("op5");
op5.addContext(contextB);
op6 = new TestOperation("op6");
op6.addContext(contextC);
op6.addContext(contextA);
history.execute(op1, null, null);
history.execute(op2, null, null);
history.execute(op3, null, null);
history.execute(op4, null, null);
history.execute(op5, null, null);
history.execute(op6, null, null);
preExec = 0; postExec = 0;
preUndo = 0; postUndo = 0;
preRedo = 0; postRedo = 0;
add = 0; remove = 0; notOK = 0;
listener = new IOperationHistoryListener() {
public void historyNotification(OperationHistoryEvent event) {
switch (event.getEventType()) {
case OperationHistoryEvent.ABOUT_TO_EXECUTE:
preExec++;
break;
case OperationHistoryEvent.ABOUT_TO_UNDO:
preUndo++;
break;
case OperationHistoryEvent.ABOUT_TO_REDO:
preRedo++;
break;
case OperationHistoryEvent.DONE:
postExec++;
break;
case OperationHistoryEvent.UNDONE:
postUndo++;
break;
case OperationHistoryEvent.REDONE:
postRedo++;
break;
case OperationHistoryEvent.OPERATION_ADDED:
add++;
break;
case OperationHistoryEvent.OPERATION_REMOVED:
remove++;
break;
case OperationHistoryEvent.OPERATION_NOT_OK:
notOK++;
break;
case OperationHistoryEvent.OPERATION_CHANGED:
changed++;
break;
}
}
};
history.addOperationHistoryListener(listener);
}
protected void tearDown() throws Exception {
super.tearDown();
history.removeOperationHistoryListener(listener);
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
}
public void testContextDispose() throws ExecutionException {
assertSame(history.getUndoOperation(contextA), op6);
assertSame(history.getUndoOperation(contextC), op6);
history.dispose(contextA, true, true, false);
assertSame(history.getUndoOperation(contextC), op6);
assertFalse(op6.hasContext(contextA));
history.undo(contextC, null, null);
history.dispose(contextC, true, false, false);
assertFalse(history.canUndo(contextC));
assertTrue(history.canRedo(contextC));
history.redo(contextC, null, null);
IUndoableOperation[] ops = history.getUndoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertEquals(ops.length, 3);
ops = history.getUndoHistory(contextC);
assertEquals(ops.length, 1);
ops = history.getUndoHistory(contextB);
assertEquals(ops.length, 2);
}
public void testContextHistories() throws ExecutionException {
assertSame(history.getUndoOperation(contextA), op6);
assertSame(history.getUndoOperation(contextB), op5);
assertSame(history.getUndoOperation(contextC), op6);
IStatus status = history.undo(contextC, null, null);
assertTrue("Status should be ok", status.isOK());
assertSame(history.getRedoOperation(contextC), op6);
assertSame(history.getUndoOperation(contextC), op3);
assertTrue("Should be able to redo in c3", history.canRedo(contextC));
assertTrue("Should be able to redo in c1", history.canRedo(contextA));
history.redo(contextA, null, null);
assertSame(history.getUndoOperation(contextC), op6);
assertSame(history.getUndoOperation(contextA), op6);
}
public void testHistoryLimit() throws ExecutionException {
history.setLimit(contextA, 2);
assertTrue(history.getUndoHistory(contextA).length == 2);
history.add(op1);
assertTrue(history.getUndoHistory(contextA).length == 2);
history.setLimit(contextB, 1);
assertTrue(history.getUndoHistory(contextB).length == 1);
assertFalse(op2.hasContext(contextB));
history.undo(contextB, null, null);
assertTrue(history.getRedoHistory(contextB).length == 1);
assertTrue(history.getUndoHistory(contextB).length == 0);
history.redo(contextB, null, null);
assertTrue(history.getRedoHistory(contextB).length == 0);
assertTrue(history.getUndoHistory(contextB).length == 1);
}
public void testLocalHistoryLimits() throws ExecutionException {
history.setLimit(contextC, 2);
assertTrue(history.getUndoHistory(contextC).length == 2);
// op2 should have context c3 removed as part of forcing the limit
assertFalse(op2.hasContext(contextC));
assertTrue(history.getUndoHistory(contextB).length == 2);
history.setLimit(contextB, 1);
assertTrue(history.getUndoHistory(contextB).length == 1);
history.undo(contextB, null, null);
op2.addContext(contextC);
history.add(op2);
assertSame(history.getUndoOperation(contextB), op2);
assertTrue(history.getUndoHistory(contextB).length == 1);
history.setLimit(contextA, 0);
assertTrue(history.getUndoHistory(contextA).length == 0);
history.add(op1);
assertTrue(history.getUndoHistory(contextA).length == 0);
}
public void testOpenOperation() throws ExecutionException {
// clear out history which will also reset operation execution counts
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
ICompositeOperation batch = new TriggeredOperations(op1, history);
history.openOperation(batch, IOperationHistory.EXECUTE);
op1.execute(null, null);
op2.execute(null, null);
history.add(op2);
history.execute(op3, null, null);
IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("no operations should be in history yet", op == null);
history.closeOperation(true, true, IOperationHistory.EXECUTE);
op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("Operation should be batching", op == batch);
op.removeContext(contextB);
assertFalse("Operation should not have context", op.hasContext(contextB));
}
public void test94459() throws ExecutionException {
// clear out history which will also reset operation execution counts
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
op2.execute(null, null);
ICompositeOperation batch = new TriggeredOperations(op2, history);
history.openOperation(batch, IOperationHistory.EXECUTE);
history.setLimit(contextA, 0);
op1.execute(null, null);
history.add(op1);
history.closeOperation(true, true, IOperationHistory.EXECUTE);
IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("Operation should be batching", op == batch);
assertFalse("Operation should not have context", op.hasContext(contextA));
}
public void test94459AllContextsEmpty() throws ExecutionException {
// clear out history which will also reset operation execution counts
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
op2.execute(null, null);
ICompositeOperation batch = new TriggeredOperations(op2, history);
history.openOperation(batch, IOperationHistory.EXECUTE);
history.setLimit(contextA, 0);
history.setLimit(contextB, 0);
history.setLimit(contextC, 0);
op1.execute(null, null);
history.add(op1);
history.closeOperation(true, true, IOperationHistory.EXECUTE);
IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("Operation should not have been added", op == null);
}
public void test94400() throws ExecutionException {
UnredoableTestOperation op = new UnredoableTestOperation("troubled op");
op.addContext(contextA);
history.execute(op, null, null);
assertTrue("Operation should be undoable", history.canUndo(contextA));
history.undo(contextA, null, null);
assertFalse("Operation should not be in redo history", history.getRedoOperation(contextA) == op);
assertTrue("Operation should be disposed", op.disposed);
}
public void testUnsuccessfulOpenOperation() throws ExecutionException {
// clear out history which will also reset operation execution counts
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
ICompositeOperation batch = new TriggeredOperations(op1, history);
history.openOperation(batch, IOperationHistory.EXECUTE);
op1.execute(null, null);
op2.execute(null, null);
history.add(op2);
history.execute(op3, null, null);
IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("no operations should be in history yet", op == null);
history.closeOperation(false, true, IOperationHistory.EXECUTE);
op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertNull("Unsuccessful operation should not be added to history", op);
assertTrue("NOT_OK notification should have been received", notOK == 1);
assertTrue("DONE should not be sent while batching", postExec == 0);
assertTrue("ADDED should not have been sent while batching", add == 0);
}
public void testNotAddedOpenOperation() throws ExecutionException {
// clear out history which will also reset operation execution counts
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
ICompositeOperation batch = new TriggeredOperations(op1, history);
history.openOperation(batch, IOperationHistory.EXECUTE);
op1.execute(null, null);
op2.execute(null, null);
history.add(op2);
history.execute(op3, null, null);
IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("no operations should be in history yet", op == null);
history.closeOperation(true, false, IOperationHistory.EXECUTE);
op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertNull("Operation should not be added to history", op);
assertTrue("DONE notification should have been received", postExec == 1);
assertTrue("ADDED should not have occurred or be sent while batching", add == 0);
}
public void testMultipleOpenOperation() throws ExecutionException {
// clear out history which will also reset operation execution counts
boolean failure = false;
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
ICompositeOperation comp1 = new TriggeredOperations(op1, history);
history.openOperation(comp1, IOperationHistory.EXECUTE);
op1.execute(null, null);
op2.execute(null, null);
history.add(op2);
history.execute(op3, null, null);
ICompositeOperation comp2 = new TriggeredOperations(op4, history);
try {
history.openOperation(comp2, IOperationHistory.EXECUTE);
} catch (IllegalStateException e) {
failure = true;
}
assertTrue("Exception should have been thrown for second open operation", failure);
IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertNull("Unexpected nested open should not add original", op);
history.closeOperation(true, true, IOperationHistory.EXECUTE);
op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertSame("First operation should be closed", op, comp1);
}
public void testAbortedOpenOperation() throws ExecutionException {
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
history.openOperation(new TriggeredOperations(op1, history), IOperationHistory.EXECUTE);
op1.execute(null, null);
history.execute(op2, null, null);
// flush history while operation is open
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
// op3 should be added as its own op since we flushed while open
history.add(op3);
// should really have no effect
history.closeOperation(true, true, IOperationHistory.EXECUTE);
IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("Open operation should be flushed", op == op3);
}
public void testOperationApproval() throws ExecutionException {
history.addOperationApprover(new LinearUndoEnforcer());
// the first undo should be fine
IStatus status = history.undo(contextB, null, null);
assertTrue(status.isOK());
// the second causes a linear violation on C
assertTrue(history.canUndo(contextB));
status = history.undo(contextB, null, null);
assertFalse(status.isOK());
// undo the newer C items
status = history.undo(contextC, null, null);
assertTrue(status.isOK());
status = history.undo(contextC, null, null);
assertTrue(status.isOK());
// now we should be okay in B
status = history.undo(contextB, null, null);
assertTrue(status.isOK());
history.addOperationApprover(new IOperationApprover() {
public IStatus proceedRedoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
return Status.CANCEL_STATUS;
}
public IStatus proceedUndoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
return Status.CANCEL_STATUS;
}
});
// everything should fail now
assertFalse(history.redo(contextB, null, null).isOK());
assertFalse(history.redo(contextC, null, null).isOK());
assertFalse(history.undo(contextA, null, null).isOK());
assertFalse(history.undo(contextB, null, null).isOK());
assertFalse(history.undo(contextC, null, null).isOK());
}
public void testOperationFailure() throws ExecutionException {
history.addOperationApprover(new IOperationApprover() {
public IStatus proceedRedoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
return Status.OK_STATUS;
}
public IStatus proceedUndoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
if (o == op6)
return Status.CANCEL_STATUS;
if (o == op5)
return new OperationStatus(IStatus.ERROR, "org.eclipse.ui.tests", 0, "Error", null);
return Status.OK_STATUS;
}
});
// should fail but still keep op6 on the stack since it's cancelled
IStatus status = history.undo(contextC, null, null);
assertFalse(status.isOK());
assertSame(history.getUndoOperation(contextC), op6);
// should fail since it's an error
status = history.undo(contextB, null, null);
assertFalse(status.isOK());
// operation remains on stack (see bug#92506)
assertSame(history.getUndoOperation(contextB), op5);
}
public void testOperationRedo() throws ExecutionException {
history.undo(contextB, null, null);
history.undo(contextB, null, null);
history.undo(contextC, null, null);
history.undo(contextC, null, null);
assertSame(history.getRedoOperation(contextB), op2);
assertSame(history.getUndoOperation(contextA), op4);
assertTrue(history.canUndo(contextA));
assertFalse(history.canUndo(contextB));
assertFalse(history.canUndo(contextC));
assertTrue(preUndo == 4);
assertTrue(postUndo == 4);
history.redo(contextB, null, null);
assertTrue(postRedo == 1);
assertTrue(history.canUndo(contextB));
assertTrue(history.canUndo(contextC));
}
public void testOperationUndo() throws ExecutionException {
history.undo(contextA, null, null);
history.undo(contextA, null, null);
assertSame(history.getRedoOperation(contextA), op4);
assertSame(history.getUndoOperation(contextA), op1);
history.undo(contextA, null, null);
assertTrue(preUndo == 3);
assertTrue(postUndo == 3);
assertFalse("Shouldn't be able to undo in c1", history.canUndo(contextA));
assertTrue("Should be able to undo in c2", history.canUndo(contextB));
assertTrue("Should be able to undo in c3", history.canUndo(contextC));
}
public void testHistoryFactory() {
IOperationHistory anotherHistory = OperationHistoryFactory.getOperationHistory();
assertNotNull(anotherHistory);
}
public void testOperationChanged() {
history.operationChanged(op1);
history.operationChanged(op2);
history.operationChanged(new TestOperation("New op"));
assertTrue("should not notify about changes if not in the history", changed == 2);
}
// the setup for the infamous (local conflict on top of composite and composite gets pruned) case
private void setup87675() throws ExecutionException {
// clear everything out. special setup for this test case
history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
contextA = new ObjectUndoContext("A");
contextB = new ObjectUndoContext("B");
contextC = new ObjectUndoContext("C");
contextW = new ObjectUndoContext("W");
history.addOperationApprover(new LinearUndoEnforcer());
// local edits on A, B, C are added first
IUndoableOperation op = new TestOperation("op1a");
op.addContext(contextA);
history.execute(op, null, null);
op = new TestOperation("op1b");
op.addContext(contextB);
history.execute(op, null, null);
op = new TestOperation("op1c");
op.addContext(contextC);
history.execute(op, null, null);
// now we create the "refactoring op" which touches them all
op = new TestOperation("Refactoring");
op.addContext(contextW);
op.execute(null, null);
refactor = new TriggeredOperations(op, history);
history.openOperation(refactor, IOperationHistory.EXECUTE);
localA = new TestOperation("op2a");
localA.addContext(contextA);
history.execute(localA, null, null);
localB = new TestOperation("op2b");
localB.addContext(contextB);
history.execute(localB, null, null);
localC = new TestOperation("op2c");
localC.addContext(contextC);
history.execute(localC, null, null);
// close off the composite
history.closeOperation(true, true, IOperationHistory.EXECUTE);
// subsequent local edit to C
op = new TestOperation("op3c");
op.addContext(contextC);
history.execute(op, null, null);
}
public void test87675_split() throws ExecutionException {
setup87675();
IUndoableOperation op;
// check setup
op = history.getUndoOperation(contextA);
assertTrue("Refactoring should be next op for context A", op == refactor);
op = history.getUndoOperation(contextB);
assertTrue("Refactoring should be next op for context B", op == refactor);
op = history.getUndoOperation(contextW);
assertTrue("Refactoring should be next op for context W", op == refactor);
op = history.getUndoOperation(contextC);
assertFalse("Refactoring should not be next op for context C", op == refactor);
// try a bogus undo
IStatus status = history.undo(contextW, null, null);
assertFalse("Undo should not be permitted due to linear conflict", status.isOK());
// prune the history for contextW
history.dispose(contextW, true, true, false);
// refactoring op should have been broken up into pieces
op = history.getUndoOperation(contextA);
assertTrue("Local edit A should be atomic", op == localA);
op = history.getUndoOperation(contextB);
assertTrue("Local edit B should be atomic", op == localB);
op = history.getUndoOperation(contextC);
assertFalse("Local edit C should not be refactoring edit", op == localC);
// now the refactoring C edit should be the next one
history.undo(contextC, null, null);
op = history.getUndoOperation(contextC);
assertTrue("Local edit C should be refactoring edit", op == localC);
}
public void test87675_undoredo() throws ExecutionException {
setup87675();
IUndoableOperation op;
// undo the local edit to C
history.undo(contextC, null, null);
// undo the refactoring operation via context C
history.undo(contextC, null, null);
// check that there are no new operations in the undo list for A, B, C
op = history.getUndoOperation(contextC);
assertTrue("Local edit C should be original edit", op.getLabel().equals("op1c"));
op = history.getUndoOperation(contextB);
assertTrue("Local edit B should be original edit", op.getLabel().equals("op1b"));
op = history.getUndoOperation(contextA);
assertTrue("Local edit A should be original edit", op.getLabel().equals("op1a"));
// test that the redo operation has all contexts
op = history.getRedoOperation(contextW);
assertTrue("operation should have context A", op.hasContext(contextA));
assertTrue("operation should have context B", op.hasContext(contextB));
assertTrue("operation should have context C", op.hasContext(contextC));
// now redo the operation
history.redo(contextA, null, null);
// test that the next undo is our refactoring operation
op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
assertTrue("operation should have context W", op.hasContext(contextW));
// undo again and check that no side effect ops were left on the undo stack
history.undo(contextW, null, null);
op = history.getUndoOperation(contextC);
assertTrue("Local edit C should be original edit", op.getLabel().equals("op1c"));
op = history.getUndoOperation(contextB);
assertTrue("Local edit B should be original edit", op.getLabel().equals("op1b"));
op = history.getUndoOperation(contextA);
assertTrue("Local edit A should be original edit", op.getLabel().equals("op1a"));
}
}