| /******************************************************************************* |
| * Copyright (c) 2009, 2010 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.text.tests; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| import org.eclipse.core.commands.ExecutionException; |
| |
| import org.eclipse.text.edits.DeleteEdit; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.TextEdit; |
| import org.eclipse.text.undo.DocumentUndoManager; |
| import org.eclipse.text.undo.IDocumentUndoManager; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.Document; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.Position; |
| |
| /** |
| * Tests for DefaultUndoManager. |
| * |
| * @since 3.5 |
| */ |
| public class DocumentUndoManagerTest { |
| |
| /** The maximum undo level. */ |
| private static final int MAX_UNDO_LEVEL = 256; |
| |
| // --- Static data sets for comparing scenarios - obtained from capturing random |
| // data --- |
| /** Original document */ |
| private static final String INITIAL_DOCUMENT_CONTENT = "+7cyg:/F!T4KnW;0+au$t1G%(`Z|u'7'_!-k?<c\"2Y.]CwsO.r"; |
| |
| /** Replacement string */ |
| private static final String[] REPLACEMENTS = { ">", "F", "M/r-*", "-", "bl", "", "}%/#", "", "k&", "f", "\\g", |
| "c!x", "TLG-", "NPO", "Rp9u", "", "X", "W(", ")z", "oe", "", "h*", "t", "I", "X=N>", "2yt", "&Z", "2)W=", |
| ":K", "P9S", "s8t8o", "", "", "5{7", "%", "", "v3", "Wz", "sH", "3c", "8", "ol", ",6$", "94[#", ".~", "n", |
| ">", "9", "W", ",(FW", "Q", "^", "Bq", "$", "re", "", "9", "8[", "Mx", "4b", "$6", "F", "8s]", "o", "-", |
| "E&6", "S\\", "/", "z.a", "4ai", "b", ")", "", "l", "VU", "7M+Ql", "xZ?x", "xx", "lc", "b", "A", "!", |
| "4pSU", "", "{J", "H", "l>_", "n&9", "", "&`", ";igQxq", "", ">", ";\"", "k\\`]G", "o{?", "", "K", "_6", |
| "=" }; |
| |
| /** Position/offset pairs */ |
| private static final int[] POSITIONS = { 18, 2, 43, 1, 3, 2, 28, 3, 35, 1, 23, 5, 32, 2, 30, 1, 22, 1, 37, 0, 23, 3, |
| 43, 2, 46, 1, 17, 1, 36, 6, 17, 5, 30, 4, 25, 1, 2, 2, 30, 0, 37, 3, 28, 1, 30, 2, 20, 5, 33, 1, 29, 1, 15, |
| 2, 21, 2, 24, 4, 38, 3, 8, 0, 33, 2, 15, 2, 25, 0, 8, 2, 20, 3, 43, 2, 44, 1, 44, 2, 32, 2, 40, 2, 32, 3, |
| 12, 2, 38, 3, 33, 2, 46, 0, 13, 3, 45, 0, 16, 2, 3, 2, 44, 0, 48, 0, 18, 5, 7, 6, 7, 3, 40, 0, 9, 1, 16, 3, |
| 28, 3, 36, 1, 35, 2, 0, 3, 6, 1, 10, 4, 14, 2, 15, 3, 33, 1, 36, 0, 37, 0, 4, 3, 31, 3, 33, 3, 11, 3, 20, 2, |
| 25, 3, 4, 3, 7, 3, 17, 0, 3, 1, 31, 3, 34, 1, 21, 0, 33, 1, 17, 4, 9, 1, 26, 3, 2, 3, 12, 1, 26, 3, 9, 5, 5, |
| 0, 31, 3, 0, 3, 12, 1, 1, 1, 3, 0, 39, 0, 9, 2, 2, 0, 28, 2 }; |
| |
| /** The undo manager. */ |
| private IDocumentUndoManager fUndoManager; |
| |
| @Before |
| public void setUp() { |
| fUndoManager = null; |
| } |
| |
| @After |
| public void tearDown() { |
| fUndoManager.disconnect(this); |
| fUndoManager = null; |
| } |
| |
| /** |
| * Test for line delimiter conversion. |
| * |
| * @throws ExecutionException if undo fails |
| * @throws BadLocationException if document change fails |
| */ |
| @Test |
| public void testConvertLineDelimiters() throws ExecutionException, BadLocationException { |
| final String original = "a\r\nb\r\n"; |
| final IDocument document = new Document(original); |
| createUndoManager(document); |
| |
| document.replace(1, 2, "\n"); |
| document.replace(3, 2, "\n"); |
| |
| assertTrue(fUndoManager.undoable()); |
| fUndoManager.undo(); |
| assertTrue(fUndoManager.undoable()); |
| fUndoManager.undo(); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(original, reverted); |
| } |
| |
| private void createUndoManager(final IDocument document) { |
| fUndoManager = new DocumentUndoManager(document); |
| fUndoManager.connect(this); |
| fUndoManager.setMaximalUndoLevel(MAX_UNDO_LEVEL); |
| } |
| |
| /** |
| * Randomly applies document changes. |
| * |
| * @throws ExecutionException if undo fails |
| * @throws BadLocationException if document change fails |
| */ |
| @Test |
| public void testRandomAccess() throws ExecutionException, BadLocationException { |
| final int RANDOM_STRING_LENGTH = 50; |
| final int RANDOM_REPLACE_COUNT = 100; |
| |
| assertTrue(RANDOM_REPLACE_COUNT >= 1); |
| assertTrue(RANDOM_REPLACE_COUNT <= MAX_UNDO_LEVEL); |
| |
| String original = createRandomString(RANDOM_STRING_LENGTH); |
| final IDocument document = new Document(original); |
| createUndoManager(document); |
| |
| doChange(document, RANDOM_REPLACE_COUNT); |
| |
| assertTrue(fUndoManager.undoable()); |
| while (fUndoManager.undoable()) |
| fUndoManager.undo(); |
| |
| final String reverted = document.get(); |
| assertEquals(original, reverted); |
| } |
| |
| private void doChange(IDocument document, int count) throws BadLocationException { |
| |
| Position[] positions = new Position[count]; |
| String[] strings = new String[count]; |
| for (int i = 0; i < count; i++) { |
| final Position position = createRandomPositionPoisson(document.getLength()); |
| final String string = createRandomStringPoisson(); |
| document.replace(position.getOffset(), position.getLength(), string); |
| positions[i] = position; |
| strings[i] = string; |
| } |
| } |
| |
| // repeatable test case for comparing success/failure among different tests |
| private void doRepeatableChange(IDocument document) throws BadLocationException { |
| assertTrue(POSITIONS.length >= (2 * REPLACEMENTS.length)); |
| for (int i = 0; i < REPLACEMENTS.length; i++) { |
| int offset = POSITIONS[i * 2]; |
| int length = POSITIONS[i * 2 + 1]; |
| if (document.getLength() > offset + length) |
| document.replace(offset, length, REPLACEMENTS[i]); |
| else |
| document.replace(0, 0, REPLACEMENTS[i]); |
| } |
| } |
| |
| @Test |
| public void testCompoundTextEdit() throws ExecutionException, BadLocationException { |
| final int RANDOM_STRING_LENGTH = 50; |
| final int RANDOM_REPLACE_COUNT = 100; |
| |
| assertTrue(RANDOM_REPLACE_COUNT >= 1); |
| assertTrue(RANDOM_REPLACE_COUNT <= MAX_UNDO_LEVEL); |
| |
| String original = createRandomString(RANDOM_STRING_LENGTH); |
| final IDocument document = new Document(original); |
| createUndoManager(document); |
| |
| // fUndoManager.beginCompoundChange(); |
| |
| MultiTextEdit fRoot = new MultiTextEdit(); |
| TextEdit e1 = new DeleteEdit(3, 1); |
| fRoot.addChild(e1); |
| fRoot.apply(document); |
| |
| fRoot = new MultiTextEdit(); |
| TextEdit e2 = new DeleteEdit(3, 1); |
| fRoot.addChild(e2); |
| fRoot.apply(document); |
| |
| |
| assertTrue(fUndoManager.undoable()); |
| fUndoManager.undo(); |
| assertFalse(fUndoManager.undoable()); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(original, reverted); |
| } |
| |
| @Test |
| public void testRandomAccessAsCompound() throws ExecutionException, BadLocationException { |
| final int RANDOM_STRING_LENGTH = 50; |
| final int RANDOM_REPLACE_COUNT = 100; |
| |
| assertTrue(RANDOM_REPLACE_COUNT >= 1); |
| assertTrue(RANDOM_REPLACE_COUNT <= MAX_UNDO_LEVEL); |
| |
| String original = createRandomString(RANDOM_STRING_LENGTH); |
| final IDocument document = new Document(original); |
| createUndoManager(document); |
| |
| fUndoManager.beginCompoundChange(); |
| doChange(document, RANDOM_REPLACE_COUNT); |
| fUndoManager.endCompoundChange(); |
| |
| assertTrue(fUndoManager.undoable()); |
| while (fUndoManager.undoable()) |
| fUndoManager.undo(); |
| assertFalse(fUndoManager.undoable()); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(original, reverted); |
| } |
| |
| /** |
| * Test case for https://bugs.eclipse.org/bugs/show_bug.cgi?id=88172 |
| * |
| * @throws ExecutionException if undo fails |
| * @throws BadLocationException if document change fails |
| */ |
| @Test |
| public void testRandomAccessAsUnclosedCompound() throws ExecutionException, BadLocationException { |
| |
| final int RANDOM_STRING_LENGTH = 50; |
| final int RANDOM_REPLACE_COUNT = 100; |
| |
| assertTrue(RANDOM_REPLACE_COUNT >= 1); |
| assertTrue(RANDOM_REPLACE_COUNT <= MAX_UNDO_LEVEL); |
| |
| String original = createRandomString(RANDOM_STRING_LENGTH); |
| final IDocument document = new Document(original); |
| createUndoManager(document); |
| |
| fUndoManager.beginCompoundChange(); |
| doChange(document, RANDOM_REPLACE_COUNT); |
| // do not close the compound. |
| // fUndoManager.endCompoundChange(); |
| |
| assertTrue(fUndoManager.undoable()); |
| while (fUndoManager.undoable()) |
| fUndoManager.undo(); |
| assertFalse(fUndoManager.undoable()); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(original, reverted); |
| } |
| |
| @Test |
| public void testRandomAccessWithMixedCompound() throws ExecutionException, BadLocationException { |
| |
| final int RANDOM_STRING_LENGTH = 50; |
| final int RANDOM_REPLACE_COUNT = 10; |
| final int NUMBER_COMPOUNDS = 5; |
| final int NUMBER_ATOMIC_PER_COMPOUND = 3; |
| |
| assertTrue(RANDOM_REPLACE_COUNT >= 1); |
| assertTrue(NUMBER_COMPOUNDS * (1 + NUMBER_ATOMIC_PER_COMPOUND) * RANDOM_REPLACE_COUNT <= MAX_UNDO_LEVEL); |
| |
| String original = createRandomString(RANDOM_STRING_LENGTH); |
| final IDocument document = new Document(original); |
| createUndoManager(document); |
| |
| for (int i = 0; i < NUMBER_COMPOUNDS; i++) { |
| fUndoManager.beginCompoundChange(); |
| doChange(document, RANDOM_REPLACE_COUNT); |
| fUndoManager.endCompoundChange(); |
| assertTrue(fUndoManager.undoable()); |
| for (int j = 0; j < NUMBER_ATOMIC_PER_COMPOUND; j++) { |
| doChange(document, RANDOM_REPLACE_COUNT); |
| assertTrue(fUndoManager.undoable()); |
| } |
| } |
| |
| assertTrue(fUndoManager.undoable()); |
| while (fUndoManager.undoable()) |
| fUndoManager.undo(); |
| assertFalse(fUndoManager.undoable()); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(original, reverted); |
| } |
| |
| @Test |
| public void testRepeatableAccess() throws ExecutionException, BadLocationException { |
| assertTrue(REPLACEMENTS.length <= MAX_UNDO_LEVEL); |
| |
| final IDocument document = new Document(INITIAL_DOCUMENT_CONTENT); |
| createUndoManager(document); |
| |
| doRepeatableChange(document); |
| |
| assertTrue(fUndoManager.undoable()); |
| while (fUndoManager.undoable()) |
| fUndoManager.undo(); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(INITIAL_DOCUMENT_CONTENT, reverted); |
| } |
| |
| @Test |
| public void testRepeatableAccessAsCompound() throws ExecutionException, BadLocationException { |
| assertTrue(REPLACEMENTS.length <= MAX_UNDO_LEVEL); |
| |
| final IDocument document = new Document(INITIAL_DOCUMENT_CONTENT); |
| createUndoManager(document); |
| |
| fUndoManager.beginCompoundChange(); |
| doRepeatableChange(document); |
| fUndoManager.endCompoundChange(); |
| |
| assertTrue(fUndoManager.undoable()); |
| fUndoManager.undo(); |
| // with a single compound, there should be only one undo |
| assertFalse(fUndoManager.undoable()); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(INITIAL_DOCUMENT_CONTENT, reverted); |
| } |
| |
| @Test |
| public void testRepeatableAccessAsUnclosedCompound() throws ExecutionException, BadLocationException { |
| assertTrue(REPLACEMENTS.length <= MAX_UNDO_LEVEL); |
| |
| final IDocument document = new Document(INITIAL_DOCUMENT_CONTENT); |
| createUndoManager(document); |
| |
| fUndoManager.beginCompoundChange(); |
| doRepeatableChange(document); |
| |
| assertTrue(fUndoManager.undoable()); |
| while (fUndoManager.undoable()) |
| fUndoManager.undo(); |
| |
| final String reverted = document.get(); |
| |
| assertEquals(INITIAL_DOCUMENT_CONTENT, reverted); |
| } |
| |
| @Test |
| public void testDocumentStamp() throws ExecutionException, BadLocationException { |
| final Document document = new Document(INITIAL_DOCUMENT_CONTENT); |
| fUndoManager = new DocumentUndoManager(document); |
| fUndoManager.connect(this); |
| |
| long stamp = document.getModificationStamp(); |
| doChange(document, 1); |
| fUndoManager.undo(); |
| assertEquals(stamp, document.getModificationStamp()); |
| |
| } |
| |
| // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=109104 |
| @Test |
| public void testDocumentStamp2() throws BadLocationException, ExecutionException { |
| final Document document = new Document(""); |
| fUndoManager = new DocumentUndoManager(document); |
| fUndoManager.connect(this); |
| |
| final int stringLength = 13; |
| document.replace(0, 0, createRandomString(stringLength)); |
| long stamp = document.getModificationStamp(); |
| fUndoManager.undo(); |
| document.replace(0, 0, createRandomString(stringLength)); |
| assertNotEquals(stamp, document.getModificationStamp()); |
| |
| } |
| |
| private static String createRandomString(int length) { |
| final StringBuilder buffer = new StringBuilder(); |
| |
| for (int i = 0; i < length; i++) |
| buffer.append(getRandomCharacter()); |
| |
| return buffer.toString(); |
| } |
| |
| private static final char getRandomCharacter() { |
| // XXX should include \t |
| return (char) (32 + 95 * Math.random()); |
| } |
| |
| private static String createRandomStringPoisson() { |
| final int length = getRandomPoissonValue(2); |
| return createRandomString(length); |
| } |
| |
| private static Position createRandomPositionPoisson(int documentLength) { |
| |
| float random = (float) Math.random(); |
| int offset = (int) (random * (documentLength + 1)); |
| |
| // Catch potential rounding issue |
| if (offset == documentLength + 1) |
| offset = documentLength; |
| |
| int length = getRandomPoissonValue(2); |
| if (offset + length > documentLength) |
| length = documentLength - offset; |
| |
| return new Position(offset, length); |
| } |
| |
| private static int getRandomPoissonValue(int mean) { |
| final int MAX_VALUE = 10; |
| |
| final float random = (float) Math.random(); |
| float probability = 0; |
| int i = 0; |
| while (probability < 1 && i < MAX_VALUE) { |
| probability += getPoissonDistribution(mean, i); |
| if (random <= probability) |
| break; |
| i++; |
| } |
| return i; |
| } |
| |
| private static float getPoissonDistribution(float lambda, int k) { |
| return (float) (Math.exp(-lambda) * Math.pow(lambda, k) / faculty(k)); |
| } |
| |
| /** |
| * Returns the faculty of k. |
| * |
| * @param k the <code>int</code> for which to get the faculty |
| * @return the faculty |
| */ |
| private static final int faculty(int k) { |
| return k == 0 ? 1 : k * faculty(k - 1); |
| } |
| |
| } |