| /******************************************************************************* |
| * Copyright (c) 2000, 2008 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.jface.text.tests.reconciler; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import junit.framework.TestCase; |
| |
| import org.eclipse.text.tests.Accessor; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.Document; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.reconciler.AbstractReconciler; |
| import org.eclipse.jface.text.reconciler.DirtyRegion; |
| import org.eclipse.jface.text.reconciler.IReconcilingStrategy; |
| import org.eclipse.jface.text.tests.TestTextViewer; |
| |
| |
| /** |
| * Reconciler tests. Uses barrier synchronization and a call log to assert |
| * correct order of reconciling events. |
| * |
| * TODO test reconciler arguments (delay > 0 etc.) |
| * TODO incremental reconciler tests |
| * |
| * @since 3.1 |
| */ |
| public class AbstractReconcilerTest extends TestCase { |
| |
| /** |
| * Modified barrier: there are two threads: the main (testing) thread |
| * creating the barrier, and the reconciler thread. When both threads have |
| * met at the barrier, the main thread is released and can perform |
| * assertions while being sure that the reconciler is dormant. After the |
| * tests have been performed, the main thread must call <code>wakeAll</code> |
| * to release the reconciler thread. |
| */ |
| static class Barrier { |
| private final Object fMutex= new Object(); |
| private final int fParticipants; |
| private final Thread fMainThread; |
| |
| private int fWaiting= 0; |
| private boolean fMainThreadArrived= false; |
| private boolean fIsInactive= false; |
| |
| Barrier() { |
| fParticipants= 2; |
| fMainThread= Thread.currentThread(); |
| } |
| |
| public void await() { |
| synchronized (fMutex) { |
| if (fIsInactive) |
| return; |
| |
| fWaiting++; |
| |
| boolean isMainThread= Thread.currentThread() == fMainThread; |
| if (isMainThread) |
| fMainThreadArrived= true; |
| |
| if (allArrived()) { |
| if (!fMainThreadArrived) { |
| fWaiting--; |
| throw new RuntimeException(getClass() + " can't join barrier if only the main thread is missing!"); |
| } |
| |
| if (!isMainThread) |
| notifyMainThread(); |
| } |
| if (!allArrived() || !isMainThread) { |
| try { |
| if (!isMainThread) |
| fMutex.wait(); |
| else { |
| fMutex.wait(5000); // don't wait forever for bad reconcilers |
| if (!allArrived()) |
| fail("reconciler never ran in 5 seconds"); |
| } |
| } catch (InterruptedException e) { |
| // threads must not be interrupted |
| throw new Error(); |
| } |
| } |
| } |
| } |
| |
| private boolean allArrived() { |
| return fWaiting == fParticipants; |
| } |
| |
| private void notifyMainThread() { |
| fMutex.notify(); |
| } |
| |
| public void wakeAll() { |
| synchronized (fMutex) { |
| fWaiting= 0; |
| fMainThreadArrived= false; |
| fMutex.notifyAll(); |
| } |
| } |
| |
| public void shutdown() { |
| synchronized (fMutex) { |
| fIsInactive= true; |
| fMutex.notifyAll(); |
| } |
| } |
| } |
| |
| private Accessor fAccessor; |
| private Barrier fBarrier; |
| private List fCallLog; |
| private ITextViewer fViewer; |
| protected AbstractReconciler fReconciler; |
| private Document fDocument; |
| |
| protected void setUp() { |
| fBarrier= new Barrier(); |
| fCallLog= Collections.synchronizedList(new ArrayList()); |
| fReconciler= new AbstractReconciler() { |
| protected void initialProcess() { |
| fCallLog.add("initialProcess"); |
| fBarrier.await(); |
| } |
| protected void process(DirtyRegion dirtyRegion) { |
| fCallLog.add("process"); |
| fBarrier.await(); |
| } |
| protected void reconcilerDocumentChanged(IDocument newDocument) { |
| fCallLog.add("reconcilerDocumentChanged"); |
| } |
| protected void aboutToBeReconciled() { |
| fCallLog.add("aboutToBeReconciled"); |
| } |
| protected void reconcilerReset() { |
| fCallLog.add("reconcilerReset"); |
| } |
| public IReconcilingStrategy getReconcilingStrategy(String contentType) { |
| return null; |
| } |
| }; |
| fReconciler.setIsIncrementalReconciler(false); |
| fReconciler.setDelay(50); // make tests run faster |
| |
| fViewer= new TestTextViewer(); |
| fReconciler.install(fViewer); |
| |
| fAccessor= new Accessor(fReconciler, AbstractReconciler.class); |
| Object object= fAccessor.get("fThread"); |
| fAccessor= new Accessor(object, object.getClass()); |
| } |
| |
| |
| protected void tearDown() throws Exception { |
| fBarrier.shutdown(); |
| fReconciler.uninstall(); |
| } |
| |
| public void testInitialReconcile() throws InterruptedException { |
| // initially the reconciler is neither active nor dirty |
| // XXX shouldn't it be dirty? |
| assertFalse(isActive()); |
| assertFalse(isDirty()); |
| |
| // set up initial document |
| fDocument= new Document("foo"); |
| fViewer.setDocument(fDocument); |
| assertEquals("reconcilerDocumentChanged", fCallLog.remove(0)); |
| assertEquals("aboutToBeReconciled", fCallLog.remove(0)); |
| |
| fBarrier.await(); |
| assertEquals("initialProcess", fCallLog.remove(0)); |
| // XXX shouldn't it be dirty and active during initialProcess? |
| assertFalse(isActive()); |
| assertFalse(isDirty()); |
| fBarrier.wakeAll(); |
| |
| // wait until clean |
| pollUntilClean(); |
| assertFalse(isActive()); |
| assertFalse(isDirty()); |
| } |
| |
| public void testDirtyingWhenClean() throws BadLocationException, InterruptedException { |
| installDocument(); |
| |
| dirty(); |
| assertEquals("aboutToBeReconciled", fCallLog.remove(0)); |
| assertEquals("reconcilerReset", fCallLog.remove(0)); |
| |
| fBarrier.await(); |
| assertEquals("process", fCallLog.remove(0)); |
| assertTrue(isActive()); |
| assertTrue(isDirty()); |
| fBarrier.wakeAll(); |
| |
| // wait until clean |
| pollUntilClean(); |
| assertFalse(isActive()); |
| assertFalse(isDirty()); |
| } |
| |
| |
| private void dirty() throws BadLocationException { |
| fDocument.replace(0,0,"bar"); |
| } |
| |
| |
| public void testDirtyingWhenRunning() throws InterruptedException, BadLocationException { |
| installDocument(); |
| |
| dirty(); |
| fBarrier.await(); |
| assertTrue(isActive()); |
| assertTrue(isDirty()); |
| fCallLog.clear(); |
| dirty(); |
| // no aboutToBeReconciled since the reconciler is still running |
| // when the second edition comes in |
| assertEquals("reconcilerReset", fCallLog.remove(0)); |
| fBarrier.wakeAll(); |
| |
| fBarrier.await(); |
| assertEquals("process", fCallLog.remove(0)); |
| fBarrier.wakeAll(); |
| pollUntilClean(); |
| assertFalse(isActive()); |
| assertFalse(isDirty()); |
| } |
| |
| public void testCancellingWhenClean() throws InterruptedException, BadLocationException { |
| installDocument(); |
| |
| // dirty again |
| dirty(); |
| fBarrier.await(); |
| fBarrier.wakeAll(); |
| |
| // cancel |
| fCallLog.clear(); |
| fReconciler.uninstall(); |
| pollUntilInactive(); |
| assertTrue(fCallLog.isEmpty()); |
| assertFalse(isActive()); |
| // XXX fails since AbstractReconciler does not update state before leaving |
| // assertFalse(isDirty()); // fails |
| } |
| |
| public void testCancellingWhenRunning() throws InterruptedException, BadLocationException { |
| installDocument(); |
| |
| // dirty and cancel |
| dirty(); |
| fBarrier.await(); |
| fCallLog.clear(); |
| fReconciler.uninstall(); |
| fBarrier.wakeAll(); |
| pollUntilInactive(); |
| assertTrue(fCallLog.isEmpty()); |
| assertFalse(isActive()); |
| // XXX fails since AbstractReconciler does not update state before leaving |
| // assertFalse(isDirty()); |
| } |
| |
| public void testReplacingDocumentWhenClean() throws InterruptedException { |
| installDocument(); |
| |
| // replace |
| fCallLog.clear(); |
| fViewer.setDocument(new Document("bar")); |
| assertEquals("reconcilerDocumentChanged", fCallLog.remove(0)); |
| assertEquals("aboutToBeReconciled", fCallLog.remove(0)); |
| assertEquals("reconcilerReset", fCallLog.remove(0)); |
| fBarrier.await(); |
| assertEquals("process", fCallLog.remove(0)); |
| fBarrier.wakeAll(); |
| |
| pollUntilClean(); |
| assertFalse(isActive()); |
| assertFalse(isDirty()); |
| } |
| |
| public void testReplacingDocumentWhenRunning() throws InterruptedException, BadLocationException { |
| installDocument(); |
| |
| // dirty and replace |
| dirty(); |
| fBarrier.await(); |
| fCallLog.clear(); |
| fViewer.setDocument(new Document("bar")); |
| assertEquals("reconcilerDocumentChanged", fCallLog.remove(0)); |
| assertEquals("reconcilerReset", fCallLog.remove(0)); |
| assertTrue(fCallLog.isEmpty()); |
| fBarrier.wakeAll(); |
| |
| // XXX this fails, which is a bug - replacing the document should |
| // cancel the progress monitor |
| // fBarrier.await(); |
| // assertEquals("process", fCallLog.remove(0)); |
| // fBarrier.wakeAll(); |
| } |
| |
| void installDocument() throws InterruptedException { |
| fDocument= new Document("foo"); |
| fViewer.setDocument(fDocument); |
| |
| // initial process |
| fBarrier.await(); |
| fBarrier.wakeAll(); |
| |
| pollUntilClean(); |
| fCallLog.clear(); |
| } |
| |
| void pollUntilClean() throws InterruptedException { |
| // wait for reconciler to become clean |
| long start= System.currentTimeMillis(); |
| while (isDirty()) { |
| long current= System.currentTimeMillis(); |
| if (current > start + 5000) |
| fail("waited > 5s for reconciler to complete"); |
| synchronized (this) { |
| wait(50); |
| } |
| } |
| } |
| |
| void pollUntilInactive() throws InterruptedException { |
| // wait for reconciler to become clean |
| long start= System.currentTimeMillis(); |
| while (isActive()) { |
| long current= System.currentTimeMillis(); |
| if (current > start + 5000) |
| fail("waited > 5s for reconciler to complete"); |
| synchronized (this) { |
| wait(50); |
| } |
| } |
| } |
| |
| boolean isActive() { |
| Object bool= fAccessor.invoke("isActive", null); |
| return ((Boolean) bool).booleanValue(); |
| } |
| |
| boolean isDirty() { |
| Object bool= fAccessor.invoke("isDirty", null); |
| return ((Boolean) bool).booleanValue(); |
| } |
| |
| |
| } |