/******************************************************************************* * 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(); } }