/* * Copyright 2013 Guidewire Software, Inc. */ package gw.plugin.ij.framework.core; 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.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.Editor; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.event.DocumentListener; import com.intellij.openapi.editor.impl.DocumentImpl; 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.ProjectImpl; 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.ModifiableRootModel; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.impl.ProjectRootManagerImpl; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.ShutDownTracker; 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.VfsUtil; 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.testFramework.CompositeException; import com.intellij.testFramework.EditorListenerTracker; import com.intellij.testFramework.LightPlatformTestCase; import com.intellij.testFramework.TestLoggerFactory; import com.intellij.testFramework.ThreadTracker; import com.intellij.util.PatchedWeakReference; import com.intellij.util.indexing.IndexableSetContributor; import com.intellij.util.indexing.IndexedRootsProvider; import com.intellij.util.ui.UIUtil; import gw.lang.reflect.TypeSystem; import gw.lang.reflect.module.IModule; import gw.plugin.ij.core.PluginLoaderUtil; import junit.framework.TestCase; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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.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; // only used for single module test class, we can ignore it for now. protected Module myModule; protected static final Collection<File> 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; private boolean ourHaveShutdownHook; protected static boolean ourPlatformPrefixInitialized; private static Set<VirtualFile> ourEternallyLivingFilesCache; static { Logger.setFactory(TestLoggerFactory.getInstance()); } 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 void autodetectPlatformPrefix() { if (ourPlatformPrefixInitialized) { return; } URL resource = PlatformTestCase.class.getClassLoader().getResource("idea/ApplicationInfo.xml"); if (resource == null) { resource = PlatformTestCase.class.getClassLoader().getResource("idea/IdeaApplicationInfo.xml"); if (resource == null) { setPlatformPrefix("PlatformLangXml"); } else { setPlatformPrefix("Idea"); } } } 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 beforeMethod() throws Exception { super.beforeMethod(); 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(getName() + "(" + getClass().getName() + ").setUp()"); myEditorListenerTracker = new EditorListenerTracker(); myThreadTracker = new ThreadTracker(); ourTestCase = this; } @Override protected void beforeClass() throws Exception { super.beforeClass(); LOG.info(getClass().getName() + ".beforeClass()"); //FSRecords.invalidateCaches(); //TODO-dp this may be too brutal // initialize application only once initApplication(); // setup new project for each test class setUpProject(); storeSettings(); if (myProject != null) { ProjectManagerEx.getInstanceEx().openTestProject(myProject); CodeStyleSettingsManager.getInstance(myProject).setTemporarySettings(new CodeStyleSettings()); InjectedLanguageManagerImpl.pushInjectors(getProject()); } DocumentCommitThread.getInstance().clearQueue(); DocumentImpl.CHECK_DOCUMENT_CONSISTENCY = !isPerformanceTest(); } public Project getProject() { return myProject; } public final PsiManager getPsiManager() { return PsiManager.getInstance(myProject); } public Module getModule() { return myModule; } public IModule getGosuModule() { return TypeSystem.getExecutionEnvironment( PluginLoaderUtil.getFrom( myModule.getProject() ) ).getModule(myModule.getName()); } protected boolean isExistingProject() { return false; } protected void setUpProject() throws Exception { myProjectManager = ProjectManagerEx.getInstanceEx(); assertNotNull("Cannot instantiate ProjectManager component", myProjectManager); File projectFile = getIprFile(); myProject = createProject(projectFile, getClass().getName() + "." + getName(), isExistingProject()); myProjectManager.openTestProject(myProject); LocalFileSystem.getInstance().refreshIoFiles(myFilesToDelete); setUpModule(); setUpJdk(); LightPlatformTestCase.clearUncommittedDocuments(getProject()); //((PsiDocumentManagerImpl) PsiDocumentManager.getInstance(getProject())).clearUncommitedDocuments(); runStartupActivities(); if (!ourHaveShutdownHook) { ourHaveShutdownHook = true; registerShutdownHook(); } } @NotNull public static Project createProject(File projectFile, String creationPlace, boolean open) throws Exception { try { Project project; if (open) { project = ProjectManagerEx.getInstanceEx().loadProject(projectFile.getPath()); } else { 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; } } public static String getCreationPlace(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.toString() + (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 { String prefix = "temp_" + getTestClassName(); System.out.println("Creating temp IPR file at : " + FileUtil.getTempDirectory() + "/" + prefix ); File tempFile = FileUtil.createTempFile(prefix, 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(getTestClassName()); } 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(baseDir.getPath().replace('/', File.separatorChar), moduleName + ModuleFileType.DOT_DEFAULT_EXTENSION); FileUtil.createIfDoesntExist(moduleFile); myFilesToDelete.add(moduleFile); return new WriteAction<Module>() { @Override protected void run(Result<Module> result) throws Throwable { final VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(moduleFile); 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(); } try { LocalFileSystemImpl localFileSystem = (LocalFileSystemImpl)LocalFileSystem.getInstance(); if (localFileSystem != null) { localFileSystem.cleanupForNextTest(); } } catch (Exception e) { // ignore } LocalHistoryImpl.getInstanceImpl().cleanupForNextTest(); PatchedWeakReference.clearAll(); } private static Set<VirtualFile> eternallyLivingFiles() { if (ourEternallyLivingFilesCache != null) { return ourEternallyLivingFilesCache; } Set<VirtualFile> 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<VirtualFile> files) { for (VirtualFile each : files) { registerSurvivor(eternallyLivingFiles(), each); } } private static void registerSurvivor(Set<VirtualFile> survivors, VirtualFile file) { addSubTree(file, survivors); while (file != null && survivors.add(file)) { file = file.getParent(); } } private static void addSubTree(VirtualFile root, Set<VirtualFile> to) { if (root instanceof VirtualDirectoryImpl) { for (VirtualFile child : ((VirtualDirectoryImpl)root).getCachedChildren()) { if (child instanceof VirtualDirectoryImpl) { to.add(child); addSubTree(child, to); } } } } @Override protected void afterClass() throws Exception { CompositeException result = new CompositeException(); if (myProject != null) { try { LightPlatformTestCase.doTearDown(getProject(), ourApplication, false); } catch (Throwable e) { result.add(e); } } try { checkForSettingsDamage(); } 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) { throw IdeaLogger.ourErrorsOccurred; } assertNull("Logger errors occurred in " + getFullName(), IdeaLogger.ourErrorsOccurred); } } finally { myProjectManager = null; myProject = null; myModule = null; myFilesToDelete.clear(); super.afterClass(); } if (!result.isEmpty()) throw result; } @Override protected void afterMethod() throws Exception { CompositeException result = new CompositeException(); ourTestCase = null; try { 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); } } finally { myEditorListenerTracker = null; myThreadTracker = null; } super.afterMethod(); //To change body of overridden methods use File | Settings | File Templates. 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); } UIUtil.dispatchAllInvocationEvents(); try { if (myProject != null) { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { Disposer.dispose(myProject); ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx(); if (projectManager instanceof ProjectManagerImpl) { projectManager.closeTestProject(myProject); } } }); } } 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; } } } public synchronized void closeAndDeleteProject() { if (myProject != null) { final VirtualFile projFile = ((ProjectImpl) myProject).getStateStore().getProjectBaseDir(); final File projectFile = projFile == null ? null : VfsUtil.virtualToIoFile(projFile); if (!myProject.isDisposed()) { Disposer.dispose(myProject); } if (projectFile != null) { FileUtil.delete(projectFile); } myProject = null; } } protected void resetAllFields() { resetClassFields(getClass()); } @Override protected final <T extends Disposable> 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); ApplicationManager.getApplication().runWriteAction(new Runnable() { public void run() { ProjectRootManagerImpl.getInstance(getProject()).setProjectSdk(jdk); // set SDK on modules Module[] modules = ModuleManager.getInstance(myProject).getModules(); for (Module module : modules) { final ModifiableRootModel rootModel = ModuleRootManager.getInstance(module).getModifiableModel(); rootModel.inheritSdk(); rootModel.commit(); } } }); } @Nullable protected Sdk getTestProjectJdk() { return null; } protected boolean isRunInWriteAction() { return true; } @Override protected void invokeTestRunnable(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, String text) throws IOException { File directory = createTempDirectory(); File file = new File(directory, name); if (!file.createNewFile()) { throw new IOException("Can't create " + file); } 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 = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(temp); assert file != null : temp; return file; } @Nullable protected PsiFile getPsiFile(final Document document) { return PsiDocumentManager.getInstance(getProject()).getPsiFile(document); } @Nullable protected PsiFile getPsiFile(@NotNull Editor editor) { return PsiDocumentManager.getInstance(getProject()).getPsiFile(editor.getDocument()); } public static void initPlatformLangPrefix() { initPlatformPrefix(IDEA_MARKER_CLASS, "PlatformLangXml"); } 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); } } } public static void setPlatformPrefix(String prefix) { System.setProperty("idea.platform.prefix", prefix); } @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<VirtualFile>() { @Override protected void run(Result<VirtualFile> 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<VirtualFile>() { @Override protected void run(Result<VirtualFile> 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(); } private void registerShutdownHook() { // FIXME shutdown hooks run asynchronously and this one must always run after // the afterClass shutdown hook in StatefulTestCase ShutDownTracker.getInstance().registerShutdownTask(new Runnable() { @Override public void run() { int retries = 3; boolean resetAllowed = true; while (_afterClassExecuting.get() != ExecutionState.COMPLETE && --retries >= 0) { synchronized (_afterClassMonitor) { try { _afterClassMonitor.wait(1000); } catch (InterruptedException e) { } if (_afterClassExecuting.get() == ExecutionState.EXECUTING && resetAllowed) { retries = 10; resetAllowed = false; } } } ShutDownTracker.invokeAndWait(true, true, new Runnable() { @Override public void run() { closeAndDeleteProject(); } }); } }); } }