// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.execution;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.RunConfigurationExtension;
import com.intellij.execution.configurations.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PathsList;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.twitter.intellij.pants.metrics.PantsExternalMetricsListenerManager;
import com.twitter.intellij.pants.model.TargetAddressInfo;
import com.twitter.intellij.pants.util.PantsConstants;
import com.twitter.intellij.pants.util.PantsUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class PantsClasspathRunConfigurationExtension extends RunConfigurationExtension {
protected static final Logger LOG = Logger.getInstance(PantsClasspathRunConfigurationExtension.class);
private static final Gson gson = new Gson();
/**
* The goal of this function is to find classpath for JUnit runner.
* <p/>
* There are two ways to do so:
* 1. If Pants supports `--export-classpath-manifest-jar-only`, then only the manifest jar will be
* picked up which contains all the classpath links for a particular test.
* 2. If not, this method will collect classpath based on all known target ids from project modules.
*/
@Override
public <T extends RunConfigurationBase> void updateJavaParameters(
T configuration,
JavaParameters params,
RunnerSettings runnerSettings
) throws ExecutionException {
final Module module = findPantsModule(configuration);
if (module == null) {
return;
}
/**
* This enables dynamic classpath for this particular run and prevents argument too long errors caused by long classpaths.
*/
params.setUseDynamicClasspath(true);
final PathsList classpath = params.getClassPath();
VirtualFile manifestJar = PantsUtil.findProjectManifestJar(configuration.getProject())
.orElseThrow(() -> new ExecutionException("Pants supports manifest jar, but it is not found."));
classpath.add(manifestJar.getPath());
PantsExternalMetricsListenerManager.getInstance().logTestRunner(configuration);
}
@NotNull
public static List<String> findPublishedClasspath(@NotNull Module module) {
final List<String> result = ContainerUtil.newArrayList();
// This is type for Gson to figure the data type to deserialize
final Type type = new TypeToken<HashSet<TargetAddressInfo>>() {}.getType();
Set<TargetAddressInfo> targetInfoSet = gson.fromJson(module.getOptionValue(PantsConstants.PANTS_TARGET_ADDRESS_INFOS_KEY), type);
// The new way to find classpath by target id
for (TargetAddressInfo ta : targetInfoSet) {
result.addAll(findPublishedClasspathByTargetId(module, ta));
}
return result;
}
@NotNull
private static List<String> findPublishedClasspathByTargetId(@NotNull Module module, @NotNull TargetAddressInfo targetAddressInfo) {
final Optional<VirtualFile> classpath = PantsUtil.findDistExportClasspathDirectory(module.getProject());
if (!classpath.isPresent()) {
return Collections.emptyList();
}
// Handle classpath with target.id
List<String> paths = ContainerUtil.newArrayList();
int count = 0;
while (true) {
VirtualFile classpathLinkFolder = classpath.get().findFileByRelativePath(targetAddressInfo.getId() + "-" + count);
VirtualFile classpathLinkFile = classpath.get().findFileByRelativePath(targetAddressInfo.getId() + "-" + count + ".jar");
if (classpathLinkFolder != null && classpathLinkFolder.isDirectory()) {
paths.add(classpathLinkFolder.getPath());
break;
}
else if (classpathLinkFile != null) {
paths.add(classpathLinkFile.getPath());
count++;
}
else {
break;
}
}
return paths;
}
@NotNull
private Map<String, String> findExcludes(@NotNull Module module) {
final Map<String, String> result = new HashMap<>();
processRuntimeModules(
module,
new Processor<Module>() {
@Override
public boolean process(Module module) {
final String targets = module.getOptionValue(PantsConstants.PANTS_TARGET_ADDRESSES_KEY);
final String excludes = module.getOptionValue(PantsConstants.PANTS_LIBRARY_EXCLUDES_KEY);
for (String exclude : PantsUtil.hydrateTargetAddresses(excludes)) {
result.put(exclude, StringUtil.notNullize(targets, module.getName()));
}
return true;
}
}
);
return result;
}
private void processRuntimeModules(@NotNull Module module, Processor<Module> processor) {
final OrderEnumerator runtimeEnumerator = OrderEnumerator.orderEntries(module).runtimeOnly().recursively();
runtimeEnumerator.forEachModule(processor);
}
@Nullable
private <T extends RunConfigurationBase> Module findPantsModule(T configuration) {
if (!(configuration instanceof ModuleBasedConfiguration)) {
return null;
}
final RunConfigurationModule runConfigurationModule = ((ModuleBasedConfiguration) configuration).getConfigurationModule();
final Module module = runConfigurationModule.getModule();
if (module == null || !PantsUtil.isPantsModule(module)) {
return null;
}
return module;
}
@Override
protected void readExternal(@NotNull RunConfigurationBase runConfiguration, @NotNull Element element) throws InvalidDataException {
}
@Override
protected void writeExternal(@NotNull RunConfigurationBase runConfiguration, @NotNull Element element) throws WriteExternalException {
}
@Nullable
@Override
protected String getEditorTitle() {
return PantsConstants.PANTS;
}
@Override
protected boolean isApplicableFor(@NotNull RunConfigurationBase configuration) {
return findPantsModule(configuration) != null;
}
@Nullable
@Override
protected <T extends RunConfigurationBase> SettingsEditor<T> createEditor(@NotNull T configuration) {
return null;
}
}