/* * Copyright 2000-2014 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.testFramework; import com.intellij.history.integration.LocalHistoryImpl; import com.intellij.ide.highlighter.ModuleFileType; import com.intellij.ide.highlighter.ProjectFileType; import com.intellij.ide.startup.impl.StartupManagerImpl; import com.intellij.idea.IdeaLogger; import com.intellij.idea.IdeaTestApplication; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.DataProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.application.Result; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.command.impl.UndoManagerImpl; import com.intellij.openapi.command.undo.UndoManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.event.DocumentListener; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl; import com.intellij.openapi.module.EmptyModuleType; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleType; import com.intellij.openapi.module.impl.ModuleManagerImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ex.ProjectManagerEx; import com.intellij.openapi.project.impl.ProjectManagerImpl; import com.intellij.openapi.project.impl.TooManyProjectLeakedException; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ModuleRootModificationUtil; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.EmptyRunnable; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl; import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl; import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS; import com.intellij.openapi.vfs.newvfs.persistent.PersistentFSImpl; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.codeStyle.CodeStyleSchemes; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CodeStyleSettingsManager; import com.intellij.psi.impl.DocumentCommitThread; import com.intellij.psi.impl.PsiManagerEx; import com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl; import com.intellij.util.PlatformUtils; import com.intellij.util.indexing.IndexableSetContributor; import com.intellij.util.indexing.IndexedRootsProvider; import com.intellij.util.ui.UIUtil; import junit.framework.TestCase; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.nio.charset.Charset; import java.util.Collection; import java.util.HashSet; import java.util.Set; /** * @author yole */ public abstract class PlatformTestCase extends UsefulTestCase implements DataProvider { public static final String TEST_DIR_PREFIX = "idea_test_"; protected static IdeaTestApplication ourApplication; protected ProjectManagerEx myProjectManager; protected Project myProject; protected Module myModule; protected static final Collection myFilesToDelete = new HashSet(); protected boolean myAssertionsInTestDetected; protected static final Logger LOG = Logger.getInstance("#com.intellij.testFramework.PlatformTestCase"); public static Thread ourTestThread; private static TestCase ourTestCase = null; public static final long DEFAULT_TEST_TIME = 300L; public static long ourTestTime = DEFAULT_TEST_TIME; private EditorListenerTracker myEditorListenerTracker; private ThreadTracker myThreadTracker; protected static boolean ourPlatformPrefixInitialized; private static Set ourEternallyLivingFilesCache; static { Logger.setFactory(TestLoggerFactory.class); } protected static long getTimeRequired() { return DEFAULT_TEST_TIME; } @Nullable protected String getApplicationConfigDirPath() throws Exception { return null; } protected void initApplication() throws Exception { boolean firstTime = ourApplication == null; autodetectPlatformPrefix(); ourApplication = IdeaTestApplication.getInstance(getApplicationConfigDirPath()); ourApplication.setDataProvider(this); if (firstTime) { cleanPersistedVFSContent(); } } private static final String[] PREFIX_CANDIDATES = { "AppCode", "CppIde", "CidrCommon", "Python", "PyCharmCore", "Ruby", "UltimateLangXml", "Idea" }; public static void autodetectPlatformPrefix() { if (ourPlatformPrefixInitialized) { return; } URL resource = PlatformTestCase.class.getClassLoader().getResource("idea/ApplicationInfo.xml"); if (resource == null) { for (String candidate : PREFIX_CANDIDATES) { resource = PlatformTestCase.class.getClassLoader().getResource("META-INF/" + candidate + "Plugin.xml"); if (resource != null) { setPlatformPrefix(candidate); break; } } } } private static void cleanPersistedVFSContent() { ((PersistentFSImpl)PersistentFS.getInstance()).cleanPersistedContents(); } @Override protected CodeStyleSettings getCurrentCodeStyleSettings() { if (CodeStyleSchemes.getInstance().getCurrentScheme() == null) return new CodeStyleSettings(); return CodeStyleSettingsManager.getSettings(getProject()); } @Override protected void setUp() throws Exception { super.setUp(); if (ourTestCase != null) { String message = "Previous test " + ourTestCase + " hasn't called tearDown(). Probably overridden without super call."; ourTestCase = null; fail(message); } IdeaLogger.ourErrorsOccurred = null; LOG.info(getClass().getName() + ".setUp()"); initApplication(); myEditorListenerTracker = new EditorListenerTracker(); myThreadTracker = new ThreadTracker(); setUpProject(); storeSettings(); ourTestCase = this; if (myProject != null) { ProjectManagerEx.getInstanceEx().openTestProject(myProject); CodeStyleSettingsManager.getInstance(myProject).setTemporarySettings(new CodeStyleSettings()); InjectedLanguageManagerImpl.pushInjectors(getProject()); } DocumentCommitThread.getInstance().clearQueue(); UIUtil.dispatchAllInvocationEvents(); } public Project getProject() { return myProject; } public final PsiManager getPsiManager() { return PsiManager.getInstance(myProject); } public Module getModule() { return myModule; } protected void setUpProject() throws Exception { myProjectManager = ProjectManagerEx.getInstanceEx(); assertNotNull("Cannot instantiate ProjectManager component", myProjectManager); File projectFile = getIprFile(); myProject = doCreateProject(projectFile); myProjectManager.openTestProject(myProject); LocalFileSystem.getInstance().refreshIoFiles(myFilesToDelete); setUpModule(); setUpJdk(); LightPlatformTestCase.clearUncommittedDocuments(getProject()); runStartupActivities(); ((FileTypeManagerImpl)FileTypeManager.getInstance()).drainReDetectQueue(); } protected Project doCreateProject(File projectFile) throws Exception { return createProject(projectFile, getClass().getName() + "." + getName()); } @NotNull public static Project createProject(File projectFile, String creationPlace) { try { Project project = ProjectManagerEx.getInstanceEx().newProject(FileUtil.getNameWithoutExtension(projectFile), projectFile.getPath(), false, false); assert project != null; project.putUserData(CREATION_PLACE, creationPlace); return project; } catch (TooManyProjectLeakedException e) { StringBuilder leakers = new StringBuilder(); leakers.append("Too many projects leaked: \n"); for (Project project : e.getLeakedProjects()) { String presentableString = getCreationPlace(project); leakers.append(presentableString); leakers.append("\n"); } fail(leakers.toString()); return null; } } @NotNull public static String getCreationPlace(@NotNull Project project) { String place = project.getUserData(CREATION_PLACE); Object base; try { base = project.isDisposed() ? "" : project.getBaseDir(); } catch (Exception e) { base = " (" + e + " while getting base dir)"; } return project + (place != null ? place : "") + base; } protected void runStartupActivities() { final StartupManagerImpl startupManager = (StartupManagerImpl)StartupManager.getInstance(myProject); startupManager.runStartupActivities(); startupManager.startCacheUpdate(); startupManager.runPostStartupActivities(); } protected File getIprFile() throws IOException { File tempFile = FileUtil.createTempFile(getName() + "_", ProjectFileType.DOT_DEFAULT_EXTENSION); myFilesToDelete.add(tempFile); return tempFile; } protected void setUpModule() { new WriteCommandAction.Simple(getProject()) { @Override protected void run() throws Throwable { myModule = createMainModule(); } }.execute().throwException(); } protected Module createMainModule() throws IOException { return createModule(myProject.getName()); } protected Module createModule(@NonNls final String moduleName) { return doCreateRealModule(moduleName); } protected Module doCreateRealModule(final String moduleName) { return doCreateRealModuleIn(moduleName, myProject, getModuleType()); } protected static Module doCreateRealModuleIn(String moduleName, final Project project, final ModuleType moduleType) { final VirtualFile baseDir = project.getBaseDir(); assertNotNull(baseDir); final File moduleFile = new File(FileUtil.toSystemDependentName(baseDir.getPath()), moduleName + ModuleFileType.DOT_DEFAULT_EXTENSION); FileUtil.createIfDoesntExist(moduleFile); myFilesToDelete.add(moduleFile); return new WriteAction() { @Override protected void run(@NotNull Result result) throws Throwable { VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(moduleFile); assertNotNull(virtualFile); Module module = ModuleManager.getInstance(project).newModule(virtualFile.getPath(), moduleType.getId()); module.getModuleFile(); result.setResult(module); } }.execute().getResultObject(); } protected ModuleType getModuleType() { return EmptyModuleType.getInstance(); } public static void cleanupApplicationCaches(Project project) { if (project != null && !project.isDisposed()) { UndoManagerImpl globalInstance = (UndoManagerImpl)UndoManager.getGlobalInstance(); if (globalInstance != null) { globalInstance.dropHistoryInTests(); } ((UndoManagerImpl)UndoManager.getInstance(project)).dropHistoryInTests(); ((PsiManagerEx)PsiManager.getInstance(project)).getFileManager().cleanupForNextTest(); } LocalFileSystemImpl localFileSystem = (LocalFileSystemImpl)LocalFileSystem.getInstance(); if (localFileSystem != null) { localFileSystem.cleanupForNextTest(); } LocalHistoryImpl.getInstanceImpl().cleanupForNextTest(); } private static Set eternallyLivingFiles() { if (ourEternallyLivingFilesCache != null) { return ourEternallyLivingFilesCache; } Set survivors = new HashSet(); for (IndexedRootsProvider provider : IndexedRootsProvider.EP_NAME.getExtensions()) { for (VirtualFile file : IndexableSetContributor.getRootsToIndex(provider)) { registerSurvivor(survivors, file); } } ourEternallyLivingFilesCache = survivors; return survivors; } public static void addSurvivingFiles(@NotNull Collection files) { for (VirtualFile each : files) { registerSurvivor(eternallyLivingFiles(), each); } } private static void registerSurvivor(Set survivors, VirtualFile file) { addSubTree(file, survivors); while (file != null && survivors.add(file)) { file = file.getParent(); } } private static void addSubTree(VirtualFile root, Set to) { if (root instanceof VirtualDirectoryImpl) { for (VirtualFile child : ((VirtualDirectoryImpl)root).getCachedChildren()) { if (child instanceof VirtualDirectoryImpl) { to.add(child); addSubTree(child, to); } } } } @Override protected void tearDown() throws Exception { CompositeException result = new CompositeException(); if (myProject != null) { try { LightPlatformTestCase.doTearDown(getProject(), ourApplication, false); } catch (Throwable e) { result.add(e); } } try { CompositeException damage = checkForSettingsDamage(); result.add(damage); } catch (Throwable e) { result.add(e); } try { Project project = getProject(); disposeProject(result); if (project != null) { try { InjectedLanguageManagerImpl.checkInjectorsAreDisposed(project); } catch (AssertionError e) { result.add(e); } } try { for (final File fileToDelete : myFilesToDelete) { delete(fileToDelete); } LocalFileSystem.getInstance().refreshIoFiles(myFilesToDelete); } catch (Throwable e) { result.add(e); } if (!myAssertionsInTestDetected) { if (IdeaLogger.ourErrorsOccurred != null) { result.add(IdeaLogger.ourErrorsOccurred); } } try { super.tearDown(); } catch (Throwable e) { result.add(e); } //cleanTheWorld(); try { myEditorListenerTracker.checkListenersLeak(); } catch (AssertionError error) { result.add(error); } try { myThreadTracker.checkLeak(); } catch (AssertionError error) { result.add(error); } try { LightPlatformTestCase.checkEditorsReleased(); } catch (Throwable error) { result.add(error); } //if (directoryIndex != null) { // directoryIndex.assertAncestorConsistent(); //} } finally { myProjectManager = null; myProject = null; myModule = null; myFilesToDelete.clear(); myEditorListenerTracker = null; myThreadTracker = null; ourTestCase = null; } if (!result.isEmpty()) throw result; } private void disposeProject(@NotNull CompositeException result) /* throws nothing */ { try { DocumentCommitThread.getInstance().clearQueue(); UIUtil.dispatchAllInvocationEvents(); } catch (Exception e) { result.add(e); } try { if (myProject != null) { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { Disposer.dispose(myProject); ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx(); if (projectManager instanceof ProjectManagerImpl) { Collection projectsStillOpen = projectManager.closeTestProject(myProject); if (!projectsStillOpen.isEmpty()) { Project project = projectsStillOpen.iterator().next(); projectsStillOpen.clear(); throw new AssertionError("Test project is not disposed: " + project+";\n created in: "+getCreationPlace(project)); } } } }); } } catch (Exception e) { result.add(e); } finally { if (myProject != null) { try { PsiDocumentManager documentManager = myProject.getComponent(PsiDocumentManager.class, null); if (documentManager != null) { EditorFactory.getInstance().getEventMulticaster().removeDocumentListener((DocumentListener)documentManager); } } catch (Exception ignored) { } myProject = null; } } } protected void resetAllFields() { resetClassFields(getClass()); } @Override protected final T disposeOnTearDown(T disposable) { Disposer.register(myProject, disposable); return disposable; } private void resetClassFields(final Class aClass) { try { clearDeclaredFields(this, aClass); } catch (IllegalAccessException e) { LOG.error(e); } if (aClass == PlatformTestCase.class) return; resetClassFields(aClass.getSuperclass()); } private String getFullName() { return getClass().getName() + "." + getName(); } private void delete(File file) { boolean b = FileUtil.delete(file); if (!b && file.exists() && !myAssertionsInTestDetected) { fail("Can't delete " + file.getAbsolutePath() + " in " + getFullName()); } } protected void simulateProjectOpen() { ModuleManagerImpl mm = (ModuleManagerImpl)ModuleManager.getInstance(myProject); StartupManagerImpl sm = (StartupManagerImpl)StartupManager.getInstance(myProject); mm.projectOpened(); setUpJdk(); sm.runStartupActivities(); sm.startCacheUpdate(); // extra init for libraries sm.runPostStartupActivities(); } protected void setUpJdk() { //final ProjectJdkEx jdk = ProjectJdkUtil.getDefaultJdk("java 1.4"); final Sdk jdk = getTestProjectJdk(); // ProjectJdkImpl jdk = ProjectJdkTable.getInstance().addJdk(defaultJdk); Module[] modules = ModuleManager.getInstance(myProject).getModules(); for (Module module : modules) { ModuleRootModificationUtil.setModuleSdk(module, jdk); } } @Nullable protected Sdk getTestProjectJdk() { return null; } @Override public void runBare() throws Throwable { if (!shouldRunTest()) return; try { runBareImpl(); } finally { try { ApplicationManager.getApplication().invokeAndWait(new Runnable() { @Override public void run() { cleanupApplicationCaches(getProject()); resetAllFields(); } }, ModalityState.NON_MODAL); } catch (Throwable e) { // Ignore } } } private void runBareImpl() throws Throwable { final Throwable[] throwables = new Throwable[1]; Runnable runnable = new Runnable() { @Override public void run() { ourTestThread = Thread.currentThread(); ourTestTime = getTimeRequired(); try { try { setUp(); } catch (Throwable e) { CompositeException result = new CompositeException(e); disposeProject(result); throw result; } try { myAssertionsInTestDetected = true; runTest(); myAssertionsInTestDetected = false; } catch (Throwable e) { throwables[0] = e; throw e; } finally { tearDown(); } } catch (Throwable throwable) { if (throwables[0] == null) { // report tearDown() problems if only no exceptions thrown from runTest() throwables[0] = throwable; } } finally { ourTestThread = null; } } }; runBareRunnable(runnable); if (IdeaLogger.ourErrorsOccurred != null) { throw IdeaLogger.ourErrorsOccurred; } if (throwables[0] != null) { throw throwables[0]; } // just to make sure all deferred Runnable's to finish waitForAllLaters(); if (IdeaLogger.ourErrorsOccurred != null) { throw IdeaLogger.ourErrorsOccurred; } /* if (++LEAK_WALKS % 1 == 0) { LeakHunter.checkLeak(ApplicationManager.getApplication(), ProjectImpl.class, new Processor() { @Override public boolean process(ProjectImpl project) { return !project.isDefault() && !LightPlatformTestCase.isLight(project); } }); } */ } private static int LEAK_WALKS; private static void waitForAllLaters() throws InterruptedException, InvocationTargetException { for (int i = 0; i < 3; i++) { SwingUtilities.invokeAndWait(EmptyRunnable.getInstance()); } } protected void runBareRunnable(Runnable runnable) throws Throwable { SwingUtilities.invokeAndWait(runnable); } protected boolean isRunInWriteAction() { return true; } @Override protected void invokeTestRunnable(@NotNull final Runnable runnable) throws Exception { final Exception[] e = new Exception[1]; Runnable runnable1 = new Runnable() { @Override public void run() { try { if (ApplicationManager.getApplication().isDispatchThread() && isRunInWriteAction()) { ApplicationManager.getApplication().runWriteAction(runnable); } else { runnable.run(); } } catch (Exception e1) { e[0] = e1; } } }; if (annotatedWith(WrapInCommand.class)) { CommandProcessor.getInstance().executeCommand(myProject, runnable1, "", null); } else { runnable1.run(); } if (e[0] != null) { throw e[0]; } } @Override public Object getData(String dataId) { return myProject == null ? null : new TestDataProvider(myProject).getData(dataId); } public static File createTempDir(@NonNls final String prefix) throws IOException { return createTempDir(prefix, true); } public static File createTempDir(@NonNls final String prefix, final boolean refresh) throws IOException { final File tempDirectory = FileUtilRt.createTempDirectory(TEST_DIR_PREFIX + prefix, null, false); myFilesToDelete.add(tempDirectory); if (refresh) { getVirtualFile(tempDirectory); } return tempDirectory; } @Nullable protected static VirtualFile getVirtualFile(final File file) { return LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); } protected File createTempDirectory() throws IOException { return createTempDir(getTestName(true)); } protected File createTempDirectory(final boolean refresh) throws IOException { return createTempDir(getTestName(true), refresh); } protected File createTempFile(String name, @Nullable String text) throws IOException { File directory = createTempDirectory(); File file = new File(directory, name); if (!file.createNewFile()) { throw new IOException("Can't create " + file); } if (text != null) { FileUtil.writeToFile(file, text); } return file; } public static void setContentOnDisk(File file, byte[] bom, String content, Charset charset) throws IOException { FileOutputStream stream = new FileOutputStream(file); if (bom != null) { stream.write(bom); } OutputStreamWriter writer = new OutputStreamWriter(stream, charset); try { writer.write(content); } finally { writer.close(); } } public static VirtualFile createTempFile(@NonNls String ext, @Nullable byte[] bom, @NonNls String content, Charset charset) throws IOException { File temp = FileUtil.createTempFile("copy", "." + ext); setContentOnDisk(temp, bom, content, charset); myFilesToDelete.add(temp); final VirtualFile file = getVirtualFile(temp); assert file != null : temp; return file; } @Nullable protected PsiFile getPsiFile(final Document document) { return PsiDocumentManager.getInstance(getProject()).getPsiFile(document); } public static void initPlatformLangPrefix() { initPlatformPrefix(IDEA_MARKER_CLASS, "PlatformLangXml"); } /** * This is the main point to set up your platform prefix. This allows you to use some sub-set of * core plugin descriptors to make initialization faster (e.g. for running tests in classpath of the module where the test is located). * It is calculated by some marker class presence in classpath. * Note that it applies NEGATIVE logic for detection: prefix will be set if only marker class * is NOT present in classpath. * Also, only the very FIRST call to this method will take effect. * * @param classToTest marker class qualified name e.g. {@link #IDEA_MARKER_CLASS}. * @param prefix platform prefix to be set up if marker class not found in classpath. */ public static void initPlatformPrefix(String classToTest, String prefix) { if (!ourPlatformPrefixInitialized) { ourPlatformPrefixInitialized = true; boolean isUltimate = true; try { PlatformTestCase.class.getClassLoader().loadClass(classToTest); } catch (ClassNotFoundException e) { isUltimate = false; } if (!isUltimate) { setPlatformPrefix(prefix); } } } private static void setPlatformPrefix(String prefix) { System.setProperty(PlatformUtils.PLATFORM_PREFIX_KEY, prefix); ourPlatformPrefixInitialized = true; } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface WrapInCommand { } protected static VirtualFile createChildData(@NotNull final VirtualFile dir, @NotNull @NonNls final String name) { return new WriteAction() { @Override protected void run(@NotNull Result result) throws Throwable { result.setResult(dir.createChildData(null, name)); } }.execute().throwException().getResultObject(); } protected static VirtualFile createChildDirectory(@NotNull final VirtualFile dir, @NotNull @NonNls final String name) { return new WriteAction() { @Override protected void run(@NotNull Result result) throws Throwable { result.setResult(dir.createChildDirectory(null, name)); } }.execute().throwException().getResultObject(); } protected static void delete(@NotNull final VirtualFile file) { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { try { file.delete(null); } catch (IOException e) { fail(); } } }); } protected static void rename(@NotNull final VirtualFile vFile1, @NotNull final String newName) { new WriteCommandAction.Simple(null) { @Override protected void run() throws Throwable { vFile1.rename(this, newName); } }.execute().throwException(); } }