com.twitter.intellij.pants.testFramework.PantsIntegrationTestCase.java Source code

Java tutorial

Introduction

Here is the source code for com.twitter.intellij.pants.testFramework.PantsIntegrationTestCase.java

Source

// Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).

package com.twitter.intellij.pants.testFramework;

import com.google.common.base.Joiner;
import com.intellij.compiler.impl.ModuleCompileScope;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ProgramRunnerUtil;
import com.intellij.execution.configurations.ConfigurationFactory;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.junit.JUnitConfiguration;
import com.intellij.execution.junit.JUnitConfigurationType;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionEnvironmentBuilder;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.ide.plugins.IdeaPluginDescriptor;
import com.intellij.ide.plugins.PluginManager;
import com.intellij.ide.util.gotoByName.GotoFileModel;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.compiler.CompilerMessage;
import com.intellij.openapi.compiler.CompilerMessageCategory;
import com.intellij.openapi.extensions.PluginId;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.settings.ExternalProjectSettings;
import com.intellij.openapi.externalSystem.test.ExternalSystemImportingTestCase;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.TestDialog;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.testFramework.CompilerTester;
import com.intellij.testFramework.ThreadTracker;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.twitter.intellij.pants.execution.PantsClasspathRunConfigurationExtension;
import com.twitter.intellij.pants.model.PantsOptions;
import com.twitter.intellij.pants.settings.PantsProjectSettings;
import com.twitter.intellij.pants.util.PantsConstants;
import com.twitter.intellij.pants.util.PantsUtil;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.util.*;

import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess;

/**
 * If your integration test modifies any source files
 * please set {@link PantsIntegrationTestCase#readOnly} to false.
 *
 * @see com.twitter.intellij.pants.highlighting.PantsHighlightingIntegrationTest
 */
public abstract class PantsIntegrationTestCase extends ExternalSystemImportingTestCase {

    private final boolean readOnly;
    private PantsProjectSettings myProjectSettings;
    private String myRelativeProjectPath = null;
    private CompilerTester myCompilerTester;

    protected PantsIntegrationTestCase() {
        this(true);
    }

    protected PantsIntegrationTestCase(boolean readOnly) {
        this.readOnly = readOnly;
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        VfsRootAccess.allowRootAccess("/");
        for (String pluginId : getRequiredPluginIds()) {
            final IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId(pluginId));
            assertNotNull(pluginId + " plugin should be in classpath for integration tests!", plugin);
            assertTrue(pluginId + " is not enabled!", plugin.isEnabled());
        }

        myProjectSettings = new PantsProjectSettings();
        myCompilerTester = null;
    }

    protected String[] getRequiredPluginIds() {
        return new String[] { "org.intellij.scala" };
    }

    @Override
    protected void setUpInWriteAction() throws Exception {
        super.setUpInWriteAction();

        final Sdk sdk = JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk();
        ProjectRootManager.getInstance(myProject).setProjectSdk(sdk);

        cleanProjectRoot();

        myProjectRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(getProjectFolder());
        assertNotNull(myProjectRoot);

        final File projectDir = new File(myProjectRoot.getPath());
        for (File projectTemplateFolder : getProjectFoldersToCopy()) {
            if (!projectTemplateFolder.exists() || !projectTemplateFolder.isDirectory()) {
                fail("invalid template project path " + projectTemplateFolder.getAbsolutePath());
            }

            PantsUtil.copyDirContent(projectTemplateFolder, projectDir);
        }
    }

    protected void cleanProjectRoot() throws ExecutionException, IOException {
        final File projectDir = new File(myProjectRoot.getPath());
        assertTrue(projectDir.exists());
        if (readOnly) {
            final File originalIni = new File(projectDir, "pants.ini");
            final File originalIniCopy = new File(projectDir, "pants.ini.copy");
            if (originalIniCopy.exists()) {
                FileUtil.copy(originalIniCopy, originalIni);
            }
            // work around copyDirContent's copying of symlinks as hard links causing pants to fail
            assertTrue("Failed to clean up!", FileUtil.delete(new File(projectDir, ".pants.d")));
            // and IJ data
            assertTrue("Failed to clean up!", FileUtil.delete(new File(projectDir, ".idea")));
            for (File file : getProjectFoldersToCopy()) {
                final File[] children = file.listFiles();
                if (children == null) {
                    continue;
                }
                for (File child : children) {
                    final File copiedChild = new File(projectDir, child.getName());
                    if (copiedChild.exists()) {
                        assertTrue("Failed to clean up!", FileUtil.delete(copiedChild));
                    }
                }
            }
        } else {
            cmd("git", "reset", "--hard");
            cmd("git", "clean", "-fdx");
        }
    }

    private void cmd(String... args) throws ExecutionException {
        final GeneralCommandLine commandLine = new GeneralCommandLine(args);
        final ProcessOutput cmdOutput = PantsUtil.getCmdOutput(commandLine.withWorkDirectory(getProjectFolder()),
                null);
        assertTrue("Failed to execute: " + StringUtil.join(args, " "), cmdOutput.getExitCode() == 0);
    }

    protected void killNailgun() throws ExecutionException {
        // NB: the ideal interface here is defaultCommandLine(myProject). However,
        // not all tests call doImport therefore myProject may not always contain modules.
        final GeneralCommandLine commandLine = PantsUtil.defaultCommandLine(getProjectPath());
        commandLine.addParameter("ng-killall");
        // Wait for command to finish.
        PantsUtil.getCmdOutput(commandLine, null);
    }

    @NotNull
    abstract protected File getProjectFolder();

    @NotNull
    protected List<File> getProjectFoldersToCopy() {
        return Collections.emptyList();
    }

    @Override
    protected String getProjectPath() {
        return super.getProjectPath() + "/" + StringUtil.notNullize(myRelativeProjectPath);
    }

    @NotNull
    public CompilerTester getCompilerTester() throws Exception {
        // CompilerTester needs to be updated every time because project modules may change.
        if (myCompilerTester != null) {
            myCompilerTester.tearDown();
        }
        final List<Module> allModules = Arrays.asList(ModuleManager.getInstance(myProject).getModules());
        myCompilerTester = new CompilerTester(myProject, allModules);
        return myCompilerTester;
    }

    protected void assertProjectName(String name) {
        assertEquals(name, myProject.getName());
    }

    protected void assertScalaLibrary(String moduleName) throws Exception {
        assertModuleLibDep(moduleName, "Pants: org.scala-lang:scala-library:2.10.4");
    }

    protected void assertClassFileInModuleOutput(String className, String moduleName) throws Exception {
        assertNotNull(String.format("Didn't find %s class in %s module's output!", className, moduleName),
                findClassFile(className, moduleName));
    }

    @Nullable
    private VirtualFile findClassFile(String className, String moduleName) throws Exception {
        PantsOptions pantsOptions = PantsOptions.getPantsOptions(myProject);
        if (pantsOptions == null) {
            return null;
        }
        if (pantsOptions.has(PantsConstants.PANTS_OPTION_EXPORT_CLASSPATH_MANIFEST_JAR)) {
            VirtualFile manifestJar = PantsUtil.findProjectManifestJar(myProject);
            if (manifestJar != null) {
                return manifestJar;
            }
            return null;
        }

        ApplicationManager.getApplication().runWriteAction(new Runnable() {
            @Override
            public void run() {
                // we need to refresh because IJ might not pick all newly created symlinks
                VirtualFileManager.getInstance().refreshWithoutFileWatcher(false);
            }
        });

        final Module module = getModule(moduleName);
        final VirtualFile buildRoot = PantsUtil.findBuildRoot(module);
        assertNotNull("Can't find working dir for module '" + moduleName + "'!", buildRoot);
        assertNotNull("Compilation wasn't completed successfully!", getCompilerTester());
        List<String> compilerOutputPaths = ContainerUtil.newArrayList();

        compilerOutputPaths.addAll(PantsClasspathRunConfigurationExtension.findPublishedClasspath(module));

        for (String compilerOutputPath : compilerOutputPaths) {
            VirtualFile output = VirtualFileManager.getInstance()
                    .refreshAndFindFileByUrl(VfsUtil.pathToUrl(compilerOutputPath));
            if (output == null) {
                continue;
            }
            try {
                if (StringUtil.equalsIgnoreCase(output.getExtension(), "jar")) {
                    output = JarFileSystem.getInstance().getJarRootForLocalFile(output);
                    assertNotNull(output);
                }
                final VirtualFile classFile = output.findFileByRelativePath(className.replace('.', '/') + ".class");
                if (classFile != null) {
                    return classFile;
                }
            } catch (AssertionError assertionError) {
                // There are some access assertions in tests. Ignore them.
                assertionError.printStackTrace();
            }
        }
        return null;
    }

    protected void modify(@NonNls @NotNull String qualifiedName) {
        final PsiClass psiClass = findClassAndAssert(qualifiedName);
        final PsiFile psiFile = psiClass.getContainingFile();
        final PsiParserFacade parserFacade = PsiParserFacade.SERVICE.getInstance(myProject);
        final PsiComment comment = parserFacade.createBlockCommentFromText(psiFile.getLanguage(), "Foo");
        WriteCommandAction.runWriteCommandAction(myProject, new Runnable() {
            @Override
            public void run() {
                psiFile.add(comment);
            }
        });
    }

    @NotNull
    protected PsiClass findClassAndAssert(@NonNls @NotNull String qualifiedName) {
        final PsiClass[] classes = JavaPsiFacade.getInstance(myProject).findClasses(qualifiedName,
                GlobalSearchScope.allScope(myProject));
        assertTrue("Several classes with the same qualified name " + qualifiedName, classes.length < 2);
        assertTrue(qualifiedName + " class not found!", classes.length > 0);
        return classes[0];
    }

    protected void doImportWithDependees(@NotNull String projectFolderPathToImport) {
        myProjectSettings.setWithDependees(true);
        doImport(projectFolderPathToImport);
    }

    protected void doImport(@NotNull String projectFolderPathToImport, String... targetNames) {
        myRelativeProjectPath = projectFolderPathToImport;
        myProjectSettings.setTargetSpecs(
                PantsUtil.convertToTargetSpecs(projectFolderPathToImport, Arrays.asList(targetNames)));
        importProject();
    }

    protected void assertGotoFileContains(String filename) {
        final GotoFileModel gotoFileModel = new GotoFileModel(myProject);
        assertTrue(ArrayUtil.contains(filename, gotoFileModel.getNames(false)));
    }

    @Override
    protected void compileModules(String... moduleNames) {
        throw new AssertionError("Please use makeModules method instead!");
    }

    protected void assertCompilationFailed(final String... moduleNames) throws Exception {
        assertCompilationFailed(getModules(moduleNames));
    }

    protected void assertCompilationFailed(final Module... modules) throws Exception {
        final List<CompilerMessage> messages = compileAndGetMessages(modules);
        for (CompilerMessage message : messages) {
            if (message.getCategory() == CompilerMessageCategory.ERROR) {
                return;
            }
        }
        fail("Compilation didn't fail!\n" + messages);
    }

    /**
     * We don't use com.intellij.openapi.externalSystem.test.ExternalSystemTestCase#compileModules
     * because we want to do some assertions on myCompilerTester
     */
    protected List<String> makeModules(final String... moduleNames) throws Exception {
        return compile(getModules(moduleNames));
    }

    protected List<String> makeProject() throws Exception {
        return assertCompilerMessages(getCompilerTester().make());
    }

    protected List<String> compile(Module... modules) throws Exception {
        return assertCompilerMessages(compileAndGetMessages(modules));
    }

    private List<String> assertCompilerMessages(List<CompilerMessage> messages) {
        for (CompilerMessage message : messages) {
            final VirtualFile virtualFile = message.getVirtualFile();
            final String prettyMessage = virtualFile == null ? message.getMessage()
                    : String.format("%s at %s:%s", message.getMessage(), virtualFile.getCanonicalPath(),
                            message.getRenderTextPrefix());
            switch (message.getCategory()) {
            case ERROR:
                // Always show full error messages.

                fail("Compilation failed with error: " + Joiner.on('\n').join(messages));
                break;
            case WARNING:
                System.out.println("Compilation warning: " + prettyMessage);
                break;
            case INFORMATION:
                break;
            case STATISTICS:
                break;
            }
        }
        final List<String> rawMessages = ContainerUtil.map(messages, new Function<CompilerMessage, String>() {
            @Override
            public String fun(CompilerMessage message) {
                return message.getMessage();
            }
        });

        final String noChanges = "pants_plugin: " + PantsConstants.COMPILE_MESSAGE_NO_CHANGES_TO_COMPILE;
        final String compiledSuccessfully = "pants: SUCCESS";
        assertTrue("Compilation wasn't successful!",
                rawMessages.contains(noChanges) || rawMessages.contains(compiledSuccessfully));
        return rawMessages;
    }

    protected List<CompilerMessage> compileAndGetMessages(Module... modules) throws Exception {
        final ModuleCompileScope scope = new ModuleCompileScope(myProject, modules, true);
        return getCompilerTester().make(scope);
    }

    private Module[] getModules(final String... moduleNames) {
        final List<Module> modules = new ArrayList<Module>();
        for (String name : moduleNames) {
            modules.add(getModule(name));
        }
        return modules.toArray(new Module[modules.size()]);
    }

    /**
     * Same as super method except it doesn't check for gen modules.
     * It appeared names of gen modules are changing from time to time
     * and we can't use a determenistic one because we run tests
     * for different version of pants.
     * <p/>
     * Use assertGenModules instead.
     */
    @Override
    protected void assertModules(String... expectedNames) {
        final Module[] actual = ModuleManager.getInstance(myProject).getModules();
        final List<String> moduleNames = ContainerUtil.mapNotNull(actual, new Function<Module, String>() {
            @Override
            public String fun(Module module) {
                final String moduleName = module.getName();
                return moduleName.startsWith(".pants.d") || moduleName.startsWith("3rdparty") ? null : moduleName;
            }
        });

        assertUnorderedElementsAreEqual(moduleNames, expectedNames);
    }

    protected void assertModuleExists(String moduleName) {
        final List<String> moduleNames = ContainerUtil.mapNotNull(ModuleManager.getInstance(myProject).getModules(),
                new Function<Module, String>() {
                    @Override
                    public String fun(Module module) {
                        return module.getName();
                    }
                });

        assertContain(moduleNames, moduleName);
    }

    protected void assertGenModules(int count) {
        final List<Module> genModules = ContainerUtil.findAll(ModuleManager.getInstance(myProject).getModules(),
                new Condition<Module>() {
                    @Override
                    public boolean value(Module module) {
                        return module.getName().startsWith(".pants.d");
                    }
                });

        assertSize(count, genModules);
    }

    public void assertSuccessfulJUnitTest(String moduleName, String className) {
        final OSProcessHandler handler = runJUnitTest(moduleName, className, null);
        assertEquals("Bad exit code! Tests failed!", 0, handler.getProcess().exitValue());
    }

    public void assertSuccessfulJUnitTest(JUnitConfiguration configuration) {
        final OSProcessHandler handler = runJUnitWithConfiguration(configuration);
        assertEquals("Bad exit code! Tests failed!", 0, handler.getProcess().exitValue());
    }

    public OSProcessHandler runJUnitTest(String moduleName, String className, @Nullable String vmParams) {
        return runJUnitWithConfiguration(generateJUnitConfiguration(moduleName, className, vmParams));
    }

    @NotNull
    private OSProcessHandler runJUnitWithConfiguration(JUnitConfiguration configuration) {
        final PantsJUnitRunnerAndConfigurationSettings runnerAndConfigurationSettings = new PantsJUnitRunnerAndConfigurationSettings(
                configuration);
        final ExecutionEnvironmentBuilder environmentBuilder = ExecutionUtil
                .createEnvironment(DefaultRunExecutor.getRunExecutorInstance(), runnerAndConfigurationSettings);
        final ExecutionEnvironment environment = environmentBuilder.build();

        ProgramRunnerUtil.executeConfiguration(environment, false, false);
        final OSProcessHandler handler = (OSProcessHandler) environment.getContentToReuse().getProcessHandler();
        assertTrue(handler.waitFor());
        return handler;
    }

    @NotNull
    public JUnitConfiguration generateJUnitConfiguration(String moduleName, String className,
            @Nullable String vmParams) {
        final ConfigurationFactory factory = JUnitConfigurationType.getInstance().getConfigurationFactories()[0];
        final JUnitConfiguration runConfiguration = new JUnitConfiguration("Pants: " + className, myProject,
                factory);
        runConfiguration.setWorkingDirectory(PantsUtil.findBuildRoot(getModule(moduleName)).getCanonicalPath());
        runConfiguration.setModule(getModule(moduleName));
        runConfiguration.setName(className);
        if (StringUtil.isNotEmpty(vmParams)) {
            runConfiguration.setVMParameters(vmParams);
        }
        runConfiguration.setMainClass(findClassAndAssert(className));
        return runConfiguration;
    }

    @Override
    public void tearDown() throws Exception {
        // TODO thread leak either a IJ bug https://youtrack.jetbrains.com/issue/IDEA-155496
        // or a pants plugin bug https://github.com/pantsbuild/intellij-pants-plugin/issues/130
        // Temporarily ignore the following 'leaking' threads to pass CI.
        ThreadTracker.longRunningThreadCreated(ApplicationManager.getApplication(), "BaseDataReader",
                "ProcessWaitFor", "Timer");
        try {
            if (myCompilerTester != null) {
                myCompilerTester.tearDown();
            }

            // Kill nailgun after usage as memory on travis is limited, at a cost of slower later builds.
            killNailgun();
            cleanProjectRoot();
            Messages.setTestDialog(TestDialog.DEFAULT);
        } finally {
            super.tearDown();
            // double check.
            if (myProject != null && !myProject.isDisposed()) {
                Disposer.dispose(myProject);
            }
        }
    }

    @Override
    protected void importProject(@NonNls @Language("Python") String config) throws IOException {
        super.importProject(config);
    }

    @Override
    protected ExternalProjectSettings getCurrentExternalProjectSettings() {
        return myProjectSettings;
    }

    @Override
    protected ProjectSystemId getExternalSystemId() {
        return PantsConstants.SYSTEM_ID;
    }

    @Override
    protected String getTestsTempDir() {
        return "pants";
    }

    @Override
    protected String getExternalSystemConfigFileName() {
        return "BUILD";
    }
}