/******************************************************************************* * Copyright (c) 2010, 2011 Obeo. * 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: * Obeo - initial API and implementation *******************************************************************************/ package org.eclipse.mylyn.docs.intent.client.ui.test.util; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import junit.framework.AssertionFailedError; import junit.framework.TestCase; import org.eclipse.core.internal.registry.ExtensionRegistry; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceDescription; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.ContributorFactoryOSGi; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IContributor; import org.eclipse.core.runtime.IExtension; import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.ILogListener; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Plugin; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.emf.ecore.EObject; import org.eclipse.mylyn.docs.intent.client.ui.IntentEditorActivator; import org.eclipse.mylyn.docs.intent.client.ui.editor.IntentEditor; import org.eclipse.mylyn.docs.intent.client.ui.editor.IntentEditorDocument; import org.eclipse.mylyn.docs.intent.client.ui.editor.annotation.IntentAnnotation; import org.eclipse.mylyn.docs.intent.client.ui.editor.annotation.IntentAnnotationMessageType; import org.eclipse.mylyn.docs.intent.client.ui.ide.builder.IntentNature; import org.eclipse.mylyn.docs.intent.client.ui.ide.builder.ToggleNatureAction; import org.eclipse.mylyn.docs.intent.client.ui.preferences.IntentPreferenceConstants; import org.eclipse.mylyn.docs.intent.client.ui.test.IntentUITestPlugin; import org.eclipse.mylyn.docs.intent.client.ui.utils.IntentEditorOpener; import org.eclipse.mylyn.docs.intent.collab.common.query.IntentDocumentQuery; import org.eclipse.mylyn.docs.intent.collab.common.repository.IntentRepositoryInitializer; import org.eclipse.mylyn.docs.intent.collab.common.repository.IntentRepositoryManager; import org.eclipse.mylyn.docs.intent.collab.handlers.adapters.RepositoryAdapter; import org.eclipse.mylyn.docs.intent.collab.repository.Repository; import org.eclipse.mylyn.docs.intent.collab.repository.RepositoryConnectionException; import org.eclipse.mylyn.docs.intent.core.document.IntentDocument; import org.eclipse.mylyn.docs.intent.core.document.IntentSection; import org.eclipse.mylyn.docs.intent.core.document.IntentStructuredElement; import org.eclipse.mylyn.docs.intent.parser.IntentParser; import org.eclipse.mylyn.docs.intent.parser.modelingunit.ParseException; import org.eclipse.mylyn.docs.intent.parser.test.utils.FileToStringConverter; import org.eclipse.ui.PlatformUI; /** * An abstract test class providing API for managing an Intent IDE projects and editors. * * @author <a href="mailto:alex.lagarde@obeo.fr">Alex Lagarde</a> */ public abstract class AbstractIntentUITest extends TestCase implements ILogListener { /** * Id of the new Intent Project wizard. */ public static final String INTENT_NEW_PROJECT_WIZARD_ID = "org.eclipse.mylyn.docs.intent.client.ui.ide.wizards.NewIntentProjectWizard"; /** * Path of a file containing an empty Itnent document. */ protected static final String INTENT_EMPTY_DOC_PATH = "data/unit/documents/empty.intent"; /** * While waiting for a client to be notified, indicates the time to wait before testing again. */ private static final int WAITING_DELAY_MILLIS = 500; /** * The tested Intent {@link IProject}. */ protected IProject intentProject; /** * The tested Intent {@link Repository}. */ protected Repository repository; /** * A {@link RepositoryAdapter} opened on the tested Repository. */ protected RepositoryAdapter repositoryAdapter; /** * A repository listener allowing to determine wether Intent clients (compiler, synchronizer...) have been * notified or not. */ protected RepositoryListenerForTests repositoryListener; /** * The tested {@link IntentDocument}. */ private IntentDocument intentDocument; /** * All the currently opened {@link IntentEditor}s. */ private List<IntentEditor> openedEditors; /** * Indicates the number of dynamically added extensions. */ private int dynamicExtensionCounter; /** * {@inheritDoc} * * @see junit.framework.TestCase#setUp() */ @Override protected void setUp() throws Exception { initClassLoader(); // Step 1 : printing testclass name (make hudson debugs easier) for (int i = 0; i < getClass().getName().length() - 1; i++) { System.out.print("="); } System.out.println("="); System.out.println(getClass().getName()); super.setUp(); // Step 2 : deactivating the automatic build of the workspace (if needed) IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspaceDescription description = workspace.getDescription(); if (description.isAutoBuilding()) { description.setAutoBuilding(false); workspace.setDescription(description); } // Step 3 : clean workspace, close welcome page WorkspaceUtils.cleanWorkspace(); WorkspaceUtils.closeWelcomePage(); waitForAllOperationsInUIThread(); IntentEditorActivator.getDefault().getLog().addLogListener(this); openedEditors = new ArrayList<IntentEditor>(); } /** * Forces OSGI to load ui.ide plugin first. Otherwise, IntentRepositoryManager.INSTANCE.getRepository is * called twice at the same time (?). Happens only in the test suite. */ private void initClassLoader() { assertNotNull(IntentNature.class); } /** * {@inheritDoc} * * @see junit.framework.TestCase#tearDown() */ @Override protected void tearDown() throws Exception { waitForAllOperationsInUIThread(); // Step 1: close editors for (IntentEditor editor : openedEditors) { editor.close(false); } // Step 2: clean workspace try { if (intentProject != null) { intentProject.delete(true, true, new NullProgressMonitor()); IntentRepositoryManager.INSTANCE.deleteRepository(intentProject.getName()); } } finally { waitForAllOperationsInUIThread(); IntentEditorActivator.getDefault().getLog().removeLogListener(this); WorkspaceUtils.cleanWorkspace(); // Step 3: unregister repository listener and deactivate advance loggin if (repositoryListener != null) { Platform.removeLogListener(repositoryListener); } IEclipsePreferences node = InstanceScope.INSTANCE.getNode(IntentEditorActivator.getDefault() .getBundle().getSymbolicName()); node.putBoolean(IntentPreferenceConstants.ACTIVATE_ADVANCE_LOGGING, false); // Step 4: setting all fields to null (to avoid memory leaks) setAllFieldsToNull(); // Step 5: remove all dynamically added contributions while (dynamicExtensionCounter > 0) { removeLastExtension(); } super.tearDown(); } } /** * Creates and register an new {@link org.eclipse.mylyn.docs.intent.collab.handlers.RepositoryClient } in * charge of detecting that events happened on the repository. */ protected final void registerRepositoryListener() { this.repositoryListener = new RepositoryListenerForTests(); // Changing preferences : activating logging IEclipsePreferences node = InstanceScope.INSTANCE.getNode(IntentEditorActivator.PLUGIN_ID); node.putBoolean(IntentPreferenceConstants.ACTIVATE_ADVANCE_LOGGING, true); Platform.addLogListener(repositoryListener); } /** * Creates a new empty Intent project. * * @param projectName * the intent project name */ protected final void setUpIntentProject(final String projectName) { setUpIntentProject(projectName, INTENT_EMPTY_DOC_PATH, false); } /** * Creates a new Intent project using the intent document located at the given path. * * @param projectName * the intent project name * @param intentDocumentPath * the path of the intent document to use (relative to the * org.eclipse.mylyn.docs.intent.client.ui.test project). */ protected final void setUpIntentProject(final String projectName, String intentDocumentPath) { setUpIntentProject(projectName, intentDocumentPath, false); } /** * Creates a new Intent project using the intent document located at the given path. * * @param projectName * the intent project name * @param intentDocumentPath * the path of the intent document to use (relative to the * org.eclipse.mylyn.docs.intent.client.ui.test project) * @param listenForRepository * indicates whether a repository listener should be registered. If you want to determine if a * client has done a job (by calling {@link AbstractIntentUITest#waitForIndexer() for example} * ), this must be true. */ protected void setUpIntentProject(final String projectName, String intentDocumentPath, boolean listenForRepository) { // Disabling preview mechanism for performances improvements IEclipsePreferences node = InstanceScope.INSTANCE.getNode(IntentEditorActivator.PLUGIN_ID); node.putBoolean(IntentPreferenceConstants.SHOW_PREVIEW_PAGE, false); try { // Step 1: getting the content of the intent document located at the given path. File file = new File(intentDocumentPath); final String intentDocumentContent = FileToStringConverter.getFileAsString(file); // Step 2: creating the intent project doCreateIntentProject(projectName, intentDocumentContent); while (repositoryAdapter == null) { Thread.sleep(10); } // Step 3: registering the repository listener if (listenForRepository) { registerRepositoryListener(); repositoryListener.clearPreviousEntries(); } // Step 4: additional setup operations additionalSetUpOperations(); } catch (CoreException e) { AssertionFailedError error = new AssertionFailedError("Failed to create Intent project"); error.setStackTrace(e.getStackTrace()); throw error; } catch (IOException e) { AssertionFailedError error = new AssertionFailedError( "Failed to get content of intent document '" + intentDocumentPath + "'"); error.setStackTrace(e.getStackTrace()); throw error; } catch (InterruptedException e) { AssertionFailedError error = new AssertionFailedError("Failed to create Intent project"); error.setStackTrace(e.getStackTrace()); throw error; } } /** * Creates the intent project with the given name, with the given content. * * @param projectName * the name of the project to create * @param intentDocumentContent * the content to associate to the created intent document * @throws CoreException * if the project cannot be created properly */ protected void doCreateIntentProject(final String projectName, final String intentDocumentContent) throws CoreException { IWorkspaceRunnable create = new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { IProject project = WorkspaceUtils.createProject(projectName, monitor); ToggleNatureAction.toggleNature(project); assertTrue("Test project has not the expected intent nature", project.hasNature("org.eclipse.mylyn.docs.intent.client.ui.ide.intentNature")); intentProject = project; setUpRepository(project); } }; ResourcesPlugin.getWorkspace().run(create, null); IntentRepositoryInitializer.initializeContent(projectName, intentDocumentContent); } /** * Set up the repository for the given project. * * @param project * the project */ protected final void setUpRepository(IProject project) { intentDocument = null; assertNotNull(project); try { repository = IntentRepositoryManager.INSTANCE.getRepository(project.getName()); assertNotNull(repository); repositoryAdapter = repository.createRepositoryAdapter(); } catch (RepositoryConnectionException e) { AssertionFailedError error = new AssertionFailedError( "Cannot connect to the created IntentRepository"); error.setStackTrace(e.getStackTrace()); throw error; } catch (CoreException e) { AssertionFailedError error = new AssertionFailedError( "Cannot retrieve the correct IntentRepository type"); error.setStackTrace(e.getStackTrace()); throw error; } // wait for initialization completed waitForAllOperationsInUIThread(); } /** * Parses the {@link IntentStructuredElement} located at the given path. If it contains an IntentDocument, * also updates the intentDocument field. * * @param intentDocumentModelPath * the path of the Intent document model (from * org.eclipse.mylyn.docs.intent.client.ui.test/data) * @return the loaded {@link IntentStructuredElement} */ protected IntentDocument parseIntentDocumentFromTests(String intentDocumentModelPath) { EObject res = null; try { res = new IntentParser().parse(FileToStringConverter.getFileAsString(new File( intentDocumentModelPath))); } catch (ParseException e) { fail(e.getMessage()); } catch (IOException e) { fail(e.getMessage()); } if (res instanceof IntentDocument) { intentDocument = (IntentDocument)res; } return intentDocument; } /** * Returns the intentDocument associated to the current Intent project. * * @return the intentDocument associated to the current Intent project */ protected final IntentDocument getIntentDocument() { if (intentDocument == null) { intentDocument = new IntentDocumentQuery(repositoryAdapter).getOrCreateIntentDocument(); } return intentDocument; } /** * Return the section at the given number. * * @param number * the number of the section * @return the section */ protected final IntentSection getIntentSection(int... number) { IntentSection section = getIntentDocument().getSubSections().get(number[0] - 1); for (int i = 1; i < number.length; i++) { section = section.getSubSections().get(number[i] - 1); } return section; } /** * Opens an editor on the Document contained in the intent project. * * @return the opened editor */ protected final IntentEditor openIntentEditor() { return openIntentEditor(getIntentDocument()); } /** * Opens an editor on the given {@link IntentStructuredElement}. * * @param element * the {@link IntentStructuredElement} to open an editor on * @return the opened editor */ protected final IntentEditor openIntentEditor(IntentStructuredElement element) { IntentEditorOpener.openIntentEditor(repository, element, true, null, true); waitForAllOperationsInUIThread(); IntentEditor editor = IntentEditorOpener.getAlreadyOpenedEditor(element); assertNotNull(editor); openedEditors.add(editor); return editor; } /** * {@inheritDoc} * * @see org.eclipse.core.runtime.ILogListener#logging(org.eclipse.core.runtime.IStatus, java.lang.String) */ public void logging(IStatus status, String plugin) { if (status.getSeverity() == IStatus.ERROR) { fail(status.getMessage()); } } /** * Wait until the end of all asynchronous operations launched in the UI Thread. */ protected static void waitForAllOperationsInUIThread() { while (PlatformUI.getWorkbench().getDisplay().readAndDispatch()) { try { Thread.sleep(10); } catch (InterruptedException e) { // Nothing to do } } } /** * Wait for synchronizer to complete work. */ protected final void waitForSynchronizer() { waitForSynchronizer(true); } /** * Wait for repository to complete work. */ protected final void waitForIndexer() { waitForIndexer(true); } /** * Wait for compiler to complete work. */ protected final void waitForCompiler() { waitForCompiler(true); } /** * Wait for the project explorer refresher to complete work. */ protected final void waitForProjectExplorerRefresher() { waitForProjectExplorerRefresher(true); } /** * Ensures that the synchronizer has been launched or not, according to the given boolean. * * @param synchronizerShouldBeNotified * indicates whether the synchronizer should be notified or not */ protected final void waitForSynchronizer(boolean synchronizerShouldBeNotified) { waitForAllOperationsInUIThread(); assertNotNull( "Cannot wait for synchronizer : you need to initialize a repository listener by calling the registerRepositoryListener() method", repositoryListener); if (synchronizerShouldBeNotified) { assertTrue("Time out : synchronizer should have handle changes but did not", repositoryListener.waitForModificationOn(synchronizerShouldBeNotified, "Synchronizer")); } else { assertFalse("Synchonizer should not have been notifed", repositoryListener.waitForModificationOn(synchronizerShouldBeNotified, "Synchronizer")); } try { Thread.sleep(WAITING_DELAY_MILLIS); } catch (InterruptedException e) { // fail silently } waitForAllOperationsInUIThread(); } /** * Ensures that the indexer has been launched or not, according to the given boolean. * * @param indexerShouldBeNotified * indicates whether the indexer should be notified or not */ protected final void waitForIndexer(boolean indexerShouldBeNotified) { waitForLifecycleMessage(indexerShouldBeNotified, "Indexer"); } /** * Ensures that the project explorer refreshed has been launched or not, according to the given boolean. * * @param refresherShouldBeNotified * indicates whether the indexer should be notified or not */ protected final void waitForProjectExplorerRefresher(boolean refresherShouldBeNotified) { waitForLifecycleMessage(refresherShouldBeNotified, "Project Explorer Refresher"); } /** * Ensures that the compiler has been launched or not, according to the given boolean. * * @param compilerShouldBeNotified * indicates whether the compiler should be notified or not */ protected final void waitForCompiler(boolean compilerShouldBeNotified) { waitForAllOperationsInUIThread(); assertNotNull( "Cannot wait for compiler : you need to initialize a repository listener by calling the registerRepositoryListener() method", repositoryListener); if (compilerShouldBeNotified) { assertTrue("Time out : compiler should have handle changes but did not", repositoryListener.waitForModificationOn(compilerShouldBeNotified, "Compiler")); } else { assertFalse("Compiler should not have been notifed", repositoryListener.waitForModificationOn(compilerShouldBeNotified, "Compiler")); } try { Thread.sleep(WAITING_DELAY_MILLIS); } catch (InterruptedException e) { // fail silently } waitForAllOperationsInUIThread(); } /** * Ensures that the client with the given identifier has been notified or not, according to the given * boolean. * * @param shouldHaveBeenNotified * indicates whether the client should be notified or not * @param intentClientID * the ID of the listened client */ protected final void waitForLifecycleMessage(boolean shouldHaveBeenNotified, String intentClientID) { waitForAllOperationsInUIThread(); assertNotNull( "Cannot wait for " + intentClientID + ": you need to initialize a repository listener by calling the registerRepositoryListener() method", repositoryListener); if (shouldHaveBeenNotified) { assertTrue("Time out: " + intentClientID + " should have handle changes but did not", repositoryListener.waitForModificationOn(shouldHaveBeenNotified, intentClientID)); } else { assertFalse(intentClientID + " should not have been notifed", repositoryListener.waitForModificationOn(shouldHaveBeenNotified, intentClientID)); } waitForAllOperationsInUIThread(); } /** * Fixes the issue associated to the Intent annotation with the given message using the quickfix. * * @param message * the annotation message * @param editor * the editor containing the annotation to search * @param document * the editor's {@link IntentEditorDocument} */ protected void fixIssueUsingQuickFix(IntentEditor editor, IntentEditorDocument document, String message) { for (IntentAnnotation annotation : AnnotationUtils.getIntentAnnotations(editor, IntentAnnotationMessageType.SYNC_WARNING)) { if (annotation.getText().equals(message)) { AnnotationUtils.applyAnnotationFix(document, repositoryAdapter, annotation, 1); editor.doSave(new NullProgressMonitor()); waitForCompiler(); waitForSynchronizer(); return; } } AnnotationUtils.displayAnnotations(editor); fail("Annotation not found: " + message); } /** * Additional operations performed at the end of set-up. */ protected void additionalSetUpOperations() { if (this.repositoryListener != null) { // waiting for synchronizer to pass waitForSynchronizer(); } } /** * Sets all fields of the current test case to null (to avoid memory leaks). */ private void setAllFieldsToNull() { // For all fields defined in the current test and all its superclasses for (Class<?> clazz = this.getClass(); clazz != TestCase.class; clazz = clazz.getSuperclass()) { for (Field field : clazz.getDeclaredFields()) { boolean isReference = !field.getType().isPrimitive(); try { field.setAccessible(true); boolean isSet = field.get(this) != null; // We do not clean non set references if (isReference && isSet) { boolean isFinal = Modifier.isFinal(field.getModifiers()); // We do not clean final fields if (!isFinal) { // Setting the field to null field.set(this, null); } } } catch (IllegalArgumentException e) { // Do nothing } catch (IllegalAccessException e) { // Do nothing } } } } /** * Dynamically adds to the extension registry a contribution from the given string. * <p> * With the given examples, this method will create the following extension :<br/> * < extension point="org.eclipse.mylyn.docs.intent.client.synchronizer.extension"><br/> * < SynchronizerExtensionDescription class="com.my.exampleclass.java"> <br/> * </ SynchronizerExtensionDescription > <br/> * </ extension > * </p> * * @param extensionPointName * the contributed extension point name (e.g. * "org.eclipse.mylyn.docs.intent.client.synchronizer.extension") * @param extensionName * the contributed extension (e.g. "SynchronizerExtensionDescription") * @param extensionValues * the extension values (e.g. ["class" => "com.my.exampleclass.java"]) * @param extensionPointPlugin * the plugin helding the registry listener (e.g. SynchronizerPlugin) * @param registryListenerFieldName * the name of the registry listener field (e.g. "registryListener") */ protected void addExtension(String extensionPointName, String extensionName, Map<String, String> extensionValues, Plugin extensionPointPlugin, String registryListenerFieldName) { try { dynamicExtensionCounter++; // Step 1: create the contribution as a string String contribution = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><plugin><extension point=\"" + extensionPointName + "\" id=\"contribution" + dynamicExtensionCounter + "\">\n"; contribution += "<" + extensionName + "\n"; for (Entry<String, String> extensionValue : extensionValues.entrySet()) { contribution += extensionValue.getKey() + "=\"" + extensionValue.getValue() + "\"\n"; } contribution += "></" + extensionName + ">\n"; contribution += "</extension></plugin>"; // Step 2: register the extension through configuration registry IExtensionRegistry registry = Platform.getExtensionRegistry(); Object token = ((ExtensionRegistry)registry).getTemporaryUserToken(); ByteArrayInputStream is = new ByteArrayInputStream(contribution.getBytes()); try { IContributor contributor = ContributorFactoryOSGi.createContributor(IntentUITestPlugin .getInstance().getBundle()); assertTrue(registry.addContribution(is, contributor, false, null, null, token)); // Work-around because registry.addContribution does dot // notifies the registryListeners Field registryListenerField = extensionPointPlugin.getClass().getDeclaredField( registryListenerFieldName); registryListenerField.setAccessible(true); Object object = registryListenerField.get(extensionPointPlugin); Method parseContributionMethods = object.getClass().getDeclaredMethod( "parseInitialContributions"); parseContributionMethods.setAccessible(true); parseContributionMethods.invoke(object); } catch (SecurityException e) { fail(e.getMessage()); } catch (IllegalArgumentException e) { fail(e.getMessage()); } catch (InvocationTargetException e) { fail(e.getMessage()); } catch (NoSuchMethodException e) { fail(e.getMessage()); } catch (NoSuchFieldException e) { fail(e.getMessage()); } finally { try { is.close(); } catch (IOException e) { fail(e.getMessage()); } } } catch (IllegalAccessException e) { fail(e.getMessage()); } } /** * Removes the last extension contributed through the * {@link AbstractIntentUITest#addExtension(Class, String, boolean)} method. */ private void removeLastExtension() { try { ExtensionRegistry extensionRegistry = (ExtensionRegistry)Platform.getExtensionRegistry(); IContributor contributor = ContributorFactoryOSGi.createContributor(IntentUITestPlugin .getInstance().getBundle()); IExtension[] extensions = extensionRegistry.getExtensions(contributor); IExtension extensionToRemove = extensions[extensions.length - 1]; dynamicExtensionCounter--; assertNotNull("Could not properly remove test extension. ", extensionToRemove); extensionRegistry.removeExtension(extensionToRemove, extensionRegistry.getTemporaryUserToken()); } catch (SecurityException e) { fail(e.getMessage()); } } /** * Reopens the current intent project, and waits for its full reactivation. */ protected void reopenIntentProject() { try { intentProject.close(new NullProgressMonitor()); waitForAllOperationsInUIThread(); intentProject.open(new NullProgressMonitor()); waitForAllOperationsInUIThread(); setUpRepository(intentProject); waitForCompiler(); } catch (CoreException e) { AssertionFailedError assertionFailedError = new AssertionFailedError( "Issues encountered while reopening Intent project: " + e.getMessage()); assertionFailedError.setStackTrace(e.getStackTrace()); throw assertionFailedError; } } }