From a35f379138ddc48921d0ca3e1ebbacb8c132b1a9 Mon Sep 17 00:00:00 2001 From: Tomasz Pasternak Date: Mon, 18 May 2020 18:57:47 +0200 Subject: [PATCH] Add GUI for `faspass amend` command (#528) Allows user to include (or remove) new pants targets into already imported project --- .../intellij/pants/PantsBundle.properties | 18 ++ resources/META-INF/pants-scala.xml | 8 + .../intellij/pants/bsp/BloopConfig.java | 18 ++ .../pants/bsp/FastpassBspAmendAction.java | 128 +++++++++ .../pants/bsp/FastpassTargetListCache.java | 26 ++ .../intellij/pants/bsp/FastpassUtils.java | 246 ++++++++++++++++++ .../pants/bsp/InvalidTargetException.java | 19 ++ .../intellij/pants/bsp/PantsBspData.java | 71 +++++ .../pants/bsp/PantsTargetAddress.java | 110 ++++++++ .../pants/bsp/PantsTargetsRepository.java | 13 + .../bsp/ui/FastpassEditTargetSpecsPanel.java | 179 +++++++++++++ .../pants/bsp/ui/FastpassManagerDialog.java | 111 ++++++++ .../intellij/pants/bsp/ui/FastpassStatus.java | 52 ++++ .../intellij/pants/bsp/ui/TargetsPreview.java | 42 +++ .../modifier/PantsSourceRootCompressor.java | 3 +- .../pants/ui/PantsToBspProjectAction.java | 30 +-- .../pants/bsp/PantsTargetAddressTest.java | 36 +++ .../integration/BspPantsIntegrationTest.java | 85 ++++++ .../AddPantsTargetDependencyFixTest.java | 8 +- .../PantsSourceRootCompressorTest.java | 2 +- 20 files changed, 1171 insertions(+), 34 deletions(-) create mode 100644 src/com/twitter/intellij/pants/bsp/BloopConfig.java create mode 100644 src/com/twitter/intellij/pants/bsp/FastpassBspAmendAction.java create mode 100644 src/com/twitter/intellij/pants/bsp/FastpassTargetListCache.java create mode 100644 src/com/twitter/intellij/pants/bsp/FastpassUtils.java create mode 100644 src/com/twitter/intellij/pants/bsp/InvalidTargetException.java create mode 100644 src/com/twitter/intellij/pants/bsp/PantsBspData.java create mode 100644 src/com/twitter/intellij/pants/bsp/PantsTargetAddress.java create mode 100644 src/com/twitter/intellij/pants/bsp/PantsTargetsRepository.java create mode 100644 src/com/twitter/intellij/pants/bsp/ui/FastpassEditTargetSpecsPanel.java create mode 100644 src/com/twitter/intellij/pants/bsp/ui/FastpassManagerDialog.java create mode 100644 src/com/twitter/intellij/pants/bsp/ui/FastpassStatus.java create mode 100644 src/com/twitter/intellij/pants/bsp/ui/TargetsPreview.java create mode 100644 tests/com/twitter/intellij/pants/bsp/PantsTargetAddressTest.java create mode 100644 tests/com/twitter/intellij/pants/integration/BspPantsIntegrationTest.java diff --git a/common/com/twitter/intellij/pants/PantsBundle.properties b/common/com/twitter/intellij/pants/PantsBundle.properties index 436ef9ce9..807118bff 100644 --- a/common/com/twitter/intellij/pants/PantsBundle.properties +++ b/common/com/twitter/intellij/pants/PantsBundle.properties @@ -56,3 +56,21 @@ pants.resolve.incremental.import.unsupported=No target root found for constructi pants.settings.text.use.intellij.compiler=Use the IntelliJ compiler [Experimental] This aims to reduce the iteration overhead by interacting with IntelliJ's compiler directly. pants.settings.text.use.intellij.compiler.help.messasge=If your project does not work with IntelliJ compiler out of the box, please try to conform the code structure to 1:1:1. pants.settings.text.use.intellij.compiler.help.messasge.link=https://www.pantsbuild.org/build_files.html#target-granularity + +pants.bsp.select.targets=Amend BSP Project +pants.bsp.error.failed.to.fetch.targets=Failed to fetch imported targets list, please look at the logs for more details +pants.bsp.error.action.not.supported.title=Action not Supported +pants.bsp.error.failed.more.than.one.bsp.project.not.supported.message=Amending targets not supported for multi-BSP root projects +pants.bsp.error.failed.not.a.bsp.pants.project.message=Amending targets is not supported for non-BSP projects +pants.bsp.error.no.project.found=No project found +pants.bsp.unknown.error=Unknown error, please look at idea.log for details +pants.bsp.invalid.targets.list=Found no matching Pants targets +pants.bsp.loading=Running "pants list" to find matching targets... +pants.bsp.preview.title.plural={0} matching Pants targets +pants.bsp.preview.title.singular=1 matching Pants target +pants.bsp.preview.title.loading=0 matching Pants targets +pants.bsp.editor.title=Targets specs (example src/main/scala::) +pants.bsp.warn.no.targets=WARNING: no targets match the specs +pants.bsp.msg.box.empty.list.content=Target specs cannot be empty +pants.bsp.msg.box.amend.title=BSP Amend +pants.bsp.msg.box.specs.unchanged.content=Target specs same as before. Nothing happens. \ No newline at end of file diff --git a/resources/META-INF/pants-scala.xml b/resources/META-INF/pants-scala.xml index ba59c2f32..6390a5c94 100644 --- a/resources/META-INF/pants-scala.xml +++ b/resources/META-INF/pants-scala.xml @@ -2,6 +2,14 @@ + + + + + + diff --git a/src/com/twitter/intellij/pants/bsp/BloopConfig.java b/src/com/twitter/intellij/pants/bsp/BloopConfig.java new file mode 100644 index 000000000..ce5acd1f0 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/BloopConfig.java @@ -0,0 +1,18 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import java.util.List; + +class BloopConfig { + private final List pantsTargets; + + public List getPantsTargets() { + return pantsTargets; + } + + BloopConfig(List pantsTargets) { + this.pantsTargets = pantsTargets; + } +} diff --git a/src/com/twitter/intellij/pants/bsp/FastpassBspAmendAction.java b/src/com/twitter/intellij/pants/bsp/FastpassBspAmendAction.java new file mode 100644 index 000000000..7a911f052 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/FastpassBspAmendAction.java @@ -0,0 +1,128 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.twitter.intellij.pants.PantsBundle; +import com.twitter.intellij.pants.bsp.ui.FastpassManagerDialog; +import com.twitter.intellij.pants.util.ExternalProjectUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.bsp.BSP; +import org.jetbrains.bsp.BspUtil; + +import javax.swing.SwingUtilities; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class FastpassBspAmendAction extends AnAction { + + private final Logger logger = Logger.getInstance(FastpassBspAmendAction.class); + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + boolean isBsp = e.getProject() != null && BspUtil.isBspProject(e.getProject()); + e.getPresentation().setEnabledAndVisible(isBsp); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + try { + Project project = event.getProject(); + if (project != null) { + Set linkedProjects = PantsBspData.importsFor(project); + if (linkedProjects.size() > 1) { + Messages.showErrorDialog( + PantsBundle.message("pants.bsp.error.failed.more.than.one.bsp.project.not.supported.message"), + PantsBundle.message("pants.bsp.error.action.not.supported.title") + ); + } + else if (linkedProjects.size() < 1) { + Messages.showErrorDialog( + PantsBundle.message("pants.bsp.error.failed.not.a.bsp.pants.project.message"), + PantsBundle.message("pants.bsp.error.action.not.supported.title") + ); + } + else { + PantsBspData importData = linkedProjects.stream().findFirst().get(); + startAmendProcedure(project, importData); + } + } + else { + Messages.showErrorDialog( + PantsBundle.message("pants.bsp.error.no.project.found"), + PantsBundle.message("pants.bsp.error.action.not.supported.title") + ); + } + } catch (Throwable e) { + logger.error(e); + } + } + + private void startAmendProcedure(Project project, PantsBspData firstProject) { + CompletableFuture> oldTargets = FastpassUtils.selectedTargets(firstProject); + + FastpassTargetListCache targetsListCache = new FastpassTargetListCache(Paths.get(firstProject.getPantsRoot().getPath())); + PantsTargetsRepository getPreview = targets -> FastpassUtils.validateAndGetPreview(firstProject.getPantsRoot(), targets, + targetsListCache::getTargetsList + ); + Optional> newTargets = FastpassManagerDialog + .promptForTargetsToImport(project, oldTargets, getPreview); + amendAndRefreshIfNeeded(project, firstProject, oldTargets, newTargets); + } + + private void amendAndRefreshIfNeeded( + @NotNull Project project, + @NotNull PantsBspData basePath, + @NotNull CompletableFuture> oldTargets, + @NotNull Optional> newTargets + ) { + oldTargets.thenAccept( + oldTargetsVal -> newTargets.ifPresent(newTargetsVal -> { + if(newTargetsVal.isEmpty()) { + SwingUtilities.invokeLater(() -> { + Messages.showErrorDialog(PantsBundle.message("pants.bsp.msg.box.empty.list.content"), + PantsBundle.message("pants.bsp.msg.box.amend.title")); + }); + } else if (!newTargetsVal.equals(oldTargetsVal)) { + try { + refreshProjectsWithNewTargetsList(project, newTargets.get(), basePath); + } + catch (Throwable e) { + logger.error(e); + } + } else { + SwingUtilities.invokeLater(() -> { + Messages.showInfoMessage(PantsBundle.message("pants.bsp.msg.box.specs.unchanged.content"), + PantsBundle.message("pants.bsp.msg.box.amend.title")); + }); + } + }) + ); + } + + private void refreshProjectsWithNewTargetsList( + @NotNull Project project, + Collection newTargets, + PantsBspData basePath + ) { + ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { + try { + FastpassUtils.amendAll(basePath, new ArrayList<>(newTargets), project).get(); + ExternalProjectUtil.refresh(project, BSP.ProjectSystemId()); + } catch (Throwable e){ + logger.error(e); + } + },"Amending", false, project ); + } +} diff --git a/src/com/twitter/intellij/pants/bsp/FastpassTargetListCache.java b/src/com/twitter/intellij/pants/bsp/FastpassTargetListCache.java new file mode 100644 index 000000000..e41d10386 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/FastpassTargetListCache.java @@ -0,0 +1,26 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import java.nio.file.Path; +import java.util.Collection; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +final public class FastpassTargetListCache { + + private final Path myPantsRoot; + + public FastpassTargetListCache(Path pantsRoot) { + + myPantsRoot = pantsRoot; + } + + final ConcurrentHashMap>> cache = new ConcurrentHashMap<>(); + + public CompletableFuture> getTargetsList(Path path) { + return cache.computeIfAbsent(path, path1 -> FastpassUtils.availableTargetsIn(myPantsRoot.resolve(path1))); + } +} \ No newline at end of file diff --git a/src/com/twitter/intellij/pants/bsp/FastpassUtils.java b/src/com/twitter/intellij/pants/bsp/FastpassUtils.java new file mode 100644 index 000000000..74bdb5113 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/FastpassUtils.java @@ -0,0 +1,246 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.VirtualFile; +import com.twitter.intellij.pants.util.PantsUtil; +import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final public class FastpassUtils { + + @NotNull + public static Stream pantsRoots(Module module) { + return Stream.of(ModuleRootManager.getInstance(module).getSourceRoots()).flatMap ( + sourceRoot -> toStream(PantsUtil.findPantsExecutable(sourceRoot.getPath()).map(VirtualFile::getParent)) + ); + } + + public static CompletableFuture amendAll(@NotNull PantsBspData importData, Collection newTargets, Project project) + throws IOException, ExecutionException { + List amendPart = Arrays.asList( + "amend", "--no-bloop-exit", "--intellij", "--intellij-launcher", "echo", + importData.getBspPath().getFileName().toString(), + "--new-targets", String.join(",", newTargets) + ); + GeneralCommandLine command = makeFastpassCommand(project, amendPart); + Process process = fastpassProcess(command, importData.getBspPath().getParent(), Paths.get(importData.getPantsRoot().getPath())); + return onExit(process).thenAccept(__ -> {}); + } + + /** + * Instead of of JDK9's CompletableFuture::onExit + */ + + @NotNull + private static CompletableFuture onExit(@NotNull Process process) { + return CompletableFuture.supplyAsync(() -> { + try { + process.waitFor(); + if (process.exitValue() != 0) { + throw new IOException(toString(process.getErrorStream())); + } + return process; + } catch (Throwable e) { + throw new CompletionException(e); + } + }); + } + + public static CompletableFuture> selectedTargets(PantsBspData basePath) { + return CompletableFuture.supplyAsync(() -> { + try { + JsonReader reader = new JsonReader(new FileReader(basePath.getBspPath().resolve(Paths.get(".bsp", "bloop.json")).toFile())); + Gson gson = new Gson(); + BloopConfig res = gson.fromJson(reader, BloopConfig.class); + return new HashSet<>(res.getPantsTargets()); + } + catch (Throwable e) { + throw new CompletionException(e); + } + }); + } + + + private static List coursierPart(){ + return Arrays.asList("launch", "org.scalameta:metals_2.12:latest.stable", + "--main", "scala.meta.internal.pantsbuild.BloopPants", + "--" + ); + } + + public static Path coursierPath() throws IOException { + Path destination = Paths.get(System.getProperty("java.io.tmpdir"), "pants-plugin-coursier"); + if (!Files.exists(destination)) { + URL url = new URL("https://git.io/coursier-cli"); + Files.copy(url.openConnection().getInputStream(), destination); + destination.toFile().setExecutable(true); + } + return destination; + } + + @NotNull + public static GeneralCommandLine makeFastpassCommand(Project project, @NotNull Collection amendPart) throws IOException { + GeneralCommandLine commandLine = PantsUtil.defaultCommandLine(project); + String coursier = FastpassUtils.coursierPath().toString(); + commandLine.setExePath(coursier); + commandLine.addParameters(coursierPart()); + commandLine.addParameters(new ArrayList<>(amendPart)); + return commandLine; + } + + @NotNull + private static Process fastpassProcess(GeneralCommandLine command, Path fastpassHome, Path pantsWorkspace) throws ExecutionException { + return command + .withWorkDirectory(pantsWorkspace.toFile()) + .withEnvironment("FASTPASS_HOME", fastpassHome.toString()) + .createProcess(); + } + + @NotNull + private static Stream toStream(@NotNull Optional pantsExecutable) { + if(pantsExecutable.isPresent()) { + return Stream.of(pantsExecutable.get()); + } else { + return Stream.empty(); + } + } + + @NotNull + private static String toString(InputStream process) throws IOException { + return IOUtils.toString(process, StandardCharsets.UTF_8); + } + + public static CompletableFuture> availableTargetsIn(Path path) { + return CompletableFuture.supplyAsync( () -> { + try { + GeneralCommandLine cmd = PantsUtil.defaultCommandLine(path.toString()); + cmd.addParameters("list", path.toString() + "::"); + Process process = cmd.createProcess(); + BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + List message = new ArrayList<>(); + String line; + while ((line = stdoutReader.readLine())!= null) { + message.add(line); + } + process.waitFor(); + if (process.exitValue() == 0) { + return message.stream().map(PantsTargetAddress::fromString).collect(Collectors.toSet()); + } else { + throw new IOException(toString(process.getErrorStream())); + } + } catch (Throwable e) { + throw new CompletionException(e); + } + }); + } + + + public static CompletableFuture>> validateAndGetDetails( + VirtualFile pantsRoot, + String targetString, + Function>> fetcher + ) { + Optional pantsTarget = PantsTargetAddress.tryParse(targetString); + if(!pantsTarget.isPresent()) { + return failedFuture(new InvalidTargetException(targetString, "Malformed address")); + } + + if(pantsRoot.findFileByRelativePath(pantsTarget.get().getPath().toString()) == null) { + return failedFuture(new InvalidTargetException(targetString, "No such folder")); + } + + return mapToSingleTargets(pantsTarget.get(), fetcher).thenApply(x -> Pair.create(pantsTarget.get(), x)); + } + + + static CompletableFuture> mapToSingleTargets( + PantsTargetAddress targetAddress, Function>> fetcher + ) { + switch (targetAddress.getKind()){ + case SINGLE_TARGET: { + CompletableFuture> fut = fetcher.apply(targetAddress.getPath()); + return fut.thenApply(targets-> { + if(targets.stream().noneMatch(target -> Objects.equals(target, targetAddress))) { + throw new CompletionException(new InvalidTargetException(targetAddress.toAddressString(), "No such target")); + } else { + return Collections.singletonList(targetAddress); + } + }); + } + case ALL_TARGETS_DEEP: { + return fetcher.apply(targetAddress.getPath()); + } + case ALL_TARGETS_FLAT: { return fetcher.apply(targetAddress.getPath()) + .thenApply(x -> x.stream() + .filter(t -> t.getPath().equals(targetAddress.getPath())) + .collect(Collectors.toSet())); + } + } + return failedFuture(new InvalidTargetException(targetAddress.toAddressString(), "Invalid kind: " + targetAddress.getKind())); + } + + /** + * Replacement for JDK9's CompletableFuture::failedFuture + */ + @NotNull + public static CompletableFuture failedFuture(Throwable ex) { + return CompletableFuture.supplyAsync(() -> + { + throw new CompletionException(ex); + }); + } + + public static CompletableFuture>> validateAndGetPreview( + VirtualFile pantsRoot, + Collection targetStrings, + Function>> fetcher + ) { + List>>> futures = + new HashSet<>(targetStrings).stream() + .map(targetString -> FastpassUtils.validateAndGetDetails(pantsRoot, targetString, fetcher)) + .collect(Collectors.toList()); + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(value -> futures.stream().map(CompletableFuture::join) + .collect(Collectors.toMap(x -> x.getFirst(), + x -> x.getSecond(), + (col1, col2 ) -> col1 + )) + ); + } +} diff --git a/src/com/twitter/intellij/pants/bsp/InvalidTargetException.java b/src/com/twitter/intellij/pants/bsp/InvalidTargetException.java new file mode 100644 index 000000000..bcda61d23 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/InvalidTargetException.java @@ -0,0 +1,19 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +public class InvalidTargetException extends Throwable { + private final String myTargetString; + private final String myMessage; + + InvalidTargetException(String targetString, String message) { + myTargetString = targetString; + myMessage = message; + } + + @Override + public String getMessage() { + return "[" + myTargetString + "]: " + myMessage; + } +} diff --git a/src/com/twitter/intellij/pants/bsp/PantsBspData.java b/src/com/twitter/intellij/pants/bsp/PantsBspData.java new file mode 100644 index 000000000..5268117d8 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/PantsBspData.java @@ -0,0 +1,71 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import com.intellij.openapi.externalSystem.ExternalSystemModulePropertyManager; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.bsp.BSP; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +final public class PantsBspData { + final private Path myBspPath; + final private VirtualFile myPantsRoot; + + public PantsBspData(Path bspPath, VirtualFile pantsRoot) { + myBspPath = bspPath; + myPantsRoot = pantsRoot; + } + + public Path getBspPath() { + return myBspPath; + } + + public VirtualFile getPantsRoot() { + return myPantsRoot; + } + + public static Set importsFor(Project project) { + return + Arrays.stream(ModuleManager.getInstance(project).getModules()) + .filter(module -> + Objects + .equals(ExternalSystemModulePropertyManager.getInstance(module).getExternalSystemId(), BSP.ProjectSystemId().getId()) && + FastpassUtils.pantsRoots(module).findFirst().isPresent() + ) + .map(module -> { + String linkedProjectPath = ExternalSystemModulePropertyManager.getInstance(module).getLinkedProjectPath(); + Optional pantsRoots = FastpassUtils.pantsRoots(module).findFirst(); + if(linkedProjectPath != null && pantsRoots.isPresent()) { + Path bspRoot = Paths.get(linkedProjectPath); + return new PantsBspData(bspRoot, pantsRoots.get()); + } else { + throw new RuntimeException("Invalid BSP-Pants import. LinkedProjectPath: " + linkedProjectPath + ", pantsRoot: " + pantsRoots.toString()); + } + }) + .collect(Collectors.toSet()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PantsBspData data = (PantsBspData) o; + return Objects.equals(myBspPath, data.myBspPath) && + Objects.equals(myPantsRoot, data.myPantsRoot); + } + + @Override + public int hashCode() { + return Objects.hash(myBspPath, myPantsRoot); + } +} \ No newline at end of file diff --git a/src/com/twitter/intellij/pants/bsp/PantsTargetAddress.java b/src/com/twitter/intellij/pants/bsp/PantsTargetAddress.java new file mode 100644 index 000000000..2015b9ac2 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/PantsTargetAddress.java @@ -0,0 +1,110 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.Optional; + +public class PantsTargetAddress { + + public enum AddressKind { + ALL_TARGETS_FLAT, + ALL_TARGETS_DEEP, + SINGLE_TARGET + } + + @NotNull private final Path myPath; + @NotNull private final AddressKind myKind; + @NotNull private final Optional myTargets; + + public static PantsTargetAddress allTargetsInDirFlat(@NotNull Path path) { + return new PantsTargetAddress(path, AddressKind.ALL_TARGETS_FLAT, Optional.empty()); + } + + public static PantsTargetAddress allTargetsInDirDeep(@NotNull Path path) { + return new PantsTargetAddress(path, AddressKind.ALL_TARGETS_DEEP, Optional.empty()); + } + + public static PantsTargetAddress oneTargetInDir(@NotNull Path path, String target) { + return new PantsTargetAddress(path, AddressKind.SINGLE_TARGET, Optional.of(target)); + } + + private PantsTargetAddress(@NotNull Path path, @NotNull AddressKind kind, @NotNull Optional target) { + myPath = path; + myKind = kind; + myTargets = target; + } + + @Override + public String toString() { + return toAddressString(); + } + + public String toAddressString() { + switch (myKind) { + case SINGLE_TARGET: return myPath + ":" + myTargets.get(); + case ALL_TARGETS_FLAT: return myPath + ":"; + case ALL_TARGETS_DEEP: return myPath + "::"; + } + throw new RuntimeException("Invalid kind: " + myKind.toString()); + } + + public static PantsTargetAddress fromString(String s) { + Optional parsed = tryParse(s); + if(parsed.isPresent()) { + return parsed.get(); + } else { + throw new RuntimeException("PantsTargetAddress: could not parse string '" + s + "'"); + } + } + + public static Optional tryParse(String s) { + String[] strings = s.split(":"); + + if (strings.length == 2) { + return Optional.of(new PantsTargetAddress(Paths.get(strings[0]), AddressKind.SINGLE_TARGET, Optional.of(strings[1]))); + } else if (strings.length >= 1 && s.endsWith("::")) { + return Optional.of(new PantsTargetAddress(Paths.get(strings[0]), AddressKind.ALL_TARGETS_DEEP, Optional.empty())); + } else if (strings.length >= 1 && s.endsWith(":")) { + return Optional.of(new PantsTargetAddress(Paths.get(strings[0]), AddressKind.ALL_TARGETS_FLAT, Optional.empty())); + } else { + return Optional.empty(); + } + } + + @NotNull + public Path getPath() { + return myPath; + } + + @NotNull + public AddressKind getKind() { + return myKind; + } + + @NotNull + public Optional getTargets() { + return myTargets; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PantsTargetAddress selection = (PantsTargetAddress) o; + return Objects.equals(getPath(), selection.getPath()) && + getKind() == selection.getKind() && + Objects.equals(getTargets(), selection.getTargets()); + } + + @Override + public int hashCode() { + return Objects.hash(getPath(), getKind(), getTargets()); + } +} + diff --git a/src/com/twitter/intellij/pants/bsp/PantsTargetsRepository.java b/src/com/twitter/intellij/pants/bsp/PantsTargetsRepository.java new file mode 100644 index 000000000..2c1b2c282 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/PantsTargetsRepository.java @@ -0,0 +1,13 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public interface PantsTargetsRepository { + CompletableFuture>> getPreview(Set rules); +} diff --git a/src/com/twitter/intellij/pants/bsp/ui/FastpassEditTargetSpecsPanel.java b/src/com/twitter/intellij/pants/bsp/ui/FastpassEditTargetSpecsPanel.java new file mode 100644 index 000000000..8fde7a946 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/ui/FastpassEditTargetSpecsPanel.java @@ -0,0 +1,179 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp.ui; + + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.ui.JBSplitter; +import com.intellij.ui.components.JBScrollPane; +import com.twitter.intellij.pants.PantsBundle; +import com.twitter.intellij.pants.bsp.InvalidTargetException; +import com.twitter.intellij.pants.bsp.PantsTargetAddress; +import com.twitter.intellij.pants.bsp.PantsTargetsRepository; +import org.jetbrains.annotations.NotNull; + +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +class FastpassEditTargetSpecsPanel extends JPanel { + private final FastpassStatus statusLabel; + private final TargetsPreview preview; + final Logger logger = Logger.getInstance(FastpassEditTargetSpecsPanel.class); + private final JLabel previewLabel; + private Set targetStrings; + + + public FastpassEditTargetSpecsPanel( + @NotNull Collection importedTargets, + @NotNull PantsTargetsRepository targetsListFetcher + ) { + myPantsTargetsRepository = targetsListFetcher; + mainPanel = new JPanel(); + statusLabel = new FastpassStatus(); + preview = new TargetsPreview(); + + previewLabel = new JLabel(); + previewLabel.setAlignmentX(LEFT_ALIGNMENT); + + editor = new JTextArea(); + editor.setText(importedTargets.stream().sorted().collect(Collectors.joining("\n"))); + onRulesListEdition(selectedTargetStrings()); + + JLabel editorLabel = new JLabel(PantsBundle.message("pants.bsp.editor.title")); + editorLabel.setAlignmentX(LEFT_ALIGNMENT); + + editor.addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent e) {} + + @Override + public void keyPressed(KeyEvent e) {} + + @Override + public void keyReleased(KeyEvent e) { + onRulesListEdition(selectedTargetStrings()); + } + }); + + this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + + + JPanel editorPanel = new JPanel(); + editorPanel.setLayout(new BoxLayout(editorPanel, BoxLayout.PAGE_AXIS)); + editorPanel.add(editorLabel); + JBScrollPane editorScroll = new JBScrollPane(editor); + editorScroll.setAlignmentX(LEFT_ALIGNMENT); + editorPanel.add(editorScroll); + + JPanel previewPanel = new JPanel(); + previewPanel.setLayout(new BoxLayout(previewPanel, BoxLayout.PAGE_AXIS)); + previewPanel.add(previewLabel); + JBScrollPane previewScroll = new JBScrollPane(preview); + previewScroll.setAlignmentX(LEFT_ALIGNMENT); + previewPanel.add(previewScroll); + + JPanel southPanel = new JPanel(); + southPanel.setLayout(new BoxLayout(southPanel, BoxLayout.LINE_AXIS)); + + JBSplitter northPanel = new JBSplitter(false); + northPanel.setFirstComponent(editorPanel); + northPanel.setSecondComponent(previewPanel); + northPanel.setProportion(0.35f); + southPanel.add(statusLabel); + + southPanel.setAlignmentX(LEFT_ALIGNMENT); + mainPanel.setAlignmentX(LEFT_ALIGNMENT); + statusLabel.setAlignmentX(LEFT_ALIGNMENT); + northPanel.setAlignmentX(LEFT_ALIGNMENT); + + mainPanel.setLayout(new BoxLayout(mainPanel,BoxLayout.PAGE_AXIS)); + mainPanel.add(northPanel); + mainPanel.add(southPanel); + + + this.add(mainPanel); + } + + private void setPreviewTitle(int matches) { + if(matches == 1 ) { + previewLabel.setText(PantsBundle.message("pants.bsp.preview.title.singular", matches)); + } else { + previewLabel.setText(PantsBundle.message("pants.bsp.preview.title.plural", matches)); + } + } + + private Set selectedTargetStrings() { + Stream lines = Arrays.stream(editor.getText().split("\n")); + return lines.filter(x -> !x.equals("")).collect(Collectors.toSet()); + } + + @NotNull final + JTextArea editor; + + final JPanel mainPanel; + + @NotNull + public Set selectedItems() { + return targetStrings; + } + + + final PantsTargetsRepository myPantsTargetsRepository; + + private void onRulesListEdition(Set targetStrings) { + this.targetStrings = targetStrings; + CompletableFuture>> + previewData = myPantsTargetsRepository.getPreview(targetStrings); + if(!previewData.isDone()) { + preview.setLoading(); + statusLabel.setLoading(); + setPreviewTitleLoading(); + } + previewData.whenComplete( + (value, error) -> SwingUtilities.invokeLater(() -> { + if (this.targetStrings == targetStrings) { + if (error == null) { + Set toPreview = value.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + setPreviewTitle(toPreview.size()); + preview.updatePreview(toPreview); + if(toPreview.size() > 0) { + statusLabel.setOk(); + } else { + statusLabel.setWarning(PantsBundle.message("pants.bsp.warn.no.targets")); + } + } + else { + preview.setError(); + if (error instanceof CompletionException && error.getCause() instanceof InvalidTargetException) { + statusLabel.setError(error.getCause().getMessage()); + } + else { + statusLabel.setError(PantsBundle.message("pants.bsp.unknown.error")); + logger.error(error); + } + } + } + }) + ); + } + + private void setPreviewTitleLoading() { + previewLabel.setText(PantsBundle.message("pants.bsp.preview.title.loading")); + } +} diff --git a/src/com/twitter/intellij/pants/bsp/ui/FastpassManagerDialog.java b/src/com/twitter/intellij/pants/bsp/ui/FastpassManagerDialog.java new file mode 100644 index 000000000..e30c626a8 --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/ui/FastpassManagerDialog.java @@ -0,0 +1,111 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp.ui; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.util.PlatformIcons; +import com.intellij.util.ui.AsyncProcessIcon; +import com.intellij.util.ui.JBUI; +import com.twitter.intellij.pants.bsp.PantsTargetsRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.intellij.CommonBundle; +import com.intellij.openapi.project.Project; +import com.twitter.intellij.pants.PantsBundle; + +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import java.awt.BorderLayout; +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +public class FastpassManagerDialog extends DialogWrapper { + public FastpassManagerDialog( + @NotNull Project project, + @NotNull CompletableFuture> importedTargets, + @NotNull PantsTargetsRepository targetsListFetcher + ) { + super(project, false); + setTitle(PantsBundle.message("pants.bsp.select.targets")); + init(); + + importedTargets.whenComplete( + (targets, error) -> + SwingUtilities.invokeLater(() -> { + if (error == null) { + showFastpassChooseTargetsPanel(targetsListFetcher, targets); + } + else { + logger.error(error); + showCurrentTargetsFetchError(); + } + })); + } + + private void showFastpassChooseTargetsPanel( + @NotNull PantsTargetsRepository targetsListFetcher, + Set targets + ) { + mainPanel.removeAll(); + myChooseTargetsPanel = new FastpassEditTargetSpecsPanel(targets, targetsListFetcher); + mainPanel.add(myChooseTargetsPanel); + setOKButtonText(CommonBundle.getOkButtonText()); + mainPanel.updateUI(); + } + + private void showCurrentTargetsFetchError() { + mainPanel.removeAll(); + mainPanel.add(new JLabel( + PantsBundle.message("pants.bsp.error.failed.to.fetch.targets"), + PlatformIcons.ERROR_INTRODUCTION_ICON, + SwingConstants.CENTER + )); + mainPanel.updateUI(); + } + + @NotNull + static final Logger logger = Logger.getInstance(FastpassManagerDialog.class); + + FastpassEditTargetSpecsPanel myChooseTargetsPanel; + + @NotNull final JPanel mainPanel = new JPanel(); + + @Nullable + @Override + protected JComponent createCenterPanel() { + mainPanel.setLayout(new BorderLayout()); + mainPanel.setPreferredSize(JBUI.size(800, 600)); + mainPanel.add(new AsyncProcessIcon(""), BorderLayout.CENTER); + return mainPanel; + } + + public Optional> selectedItems() { + return Optional.ofNullable(myChooseTargetsPanel) + .map(FastpassEditTargetSpecsPanel::selectedItems); + } + + public static Optional> promptForTargetsToImport( + Project project, + CompletableFuture> importedTargets, + PantsTargetsRepository fetchTargetsList + ) { + try { + FastpassManagerDialog dial = + new FastpassManagerDialog(project, importedTargets, fetchTargetsList); + dial.show(); + return dial.isOK() ? dial.selectedItems().map(HashSet::new) : Optional.empty(); + }catch (Throwable e) { + logger.error(e); + return Optional.empty(); + } + } +} diff --git a/src/com/twitter/intellij/pants/bsp/ui/FastpassStatus.java b/src/com/twitter/intellij/pants/bsp/ui/FastpassStatus.java new file mode 100644 index 000000000..587f7623a --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/ui/FastpassStatus.java @@ -0,0 +1,52 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp.ui; + +import com.intellij.util.PlatformIcons; +import com.intellij.util.ui.AsyncProcessIcon; + +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; + +public class FastpassStatus extends JPanel { + final JLabel myLabel; + public FastpassStatus() { + myLabel = new JLabel(); + myLabel.setText(" "); + myLabel.setAlignmentX(LEFT_ALIGNMENT); + this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + this.add(myLabel); + } + + public void setLoading(){ + this.removeAll(); + this.add(new AsyncProcessIcon( " ")); + this.updateUI(); + } + + public void setOk(){ + this.removeAll(); + this.add(myLabel); + myLabel.setIcon(PlatformIcons.CHECK_ICON); + myLabel.setText(""); + this.updateUI(); + } + + public void setWarning(String msg) { + this.removeAll(); + this.add(myLabel); + myLabel.setIcon(PlatformIcons.WARNING_INTRODUCTION_ICON); + myLabel.setText(msg); + this.updateUI(); + } + + public void setError(String msg) { + this.removeAll(); + this.add(myLabel); + myLabel.setIcon(PlatformIcons.ERROR_INTRODUCTION_ICON); + myLabel.setText(msg); + this.updateUI(); + } +} diff --git a/src/com/twitter/intellij/pants/bsp/ui/TargetsPreview.java b/src/com/twitter/intellij/pants/bsp/ui/TargetsPreview.java new file mode 100644 index 000000000..961b3dd5c --- /dev/null +++ b/src/com/twitter/intellij/pants/bsp/ui/TargetsPreview.java @@ -0,0 +1,42 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp.ui; + +import com.twitter.intellij.pants.PantsBundle; +import com.twitter.intellij.pants.bsp.PantsTargetAddress; + +import javax.swing.BoxLayout; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import java.util.Set; +import java.util.stream.Collectors; + +public class TargetsPreview extends JPanel { + private final JTextArea preview; + + public TargetsPreview() { + preview = new JTextArea(); + preview.setAlignmentX(JTextArea.LEFT_ALIGNMENT); + preview.setEditable(false); + + this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.add(preview); + } + + public void setError() { + preview.setText(PantsBundle.message("pants.bsp.invalid.targets.list")); + } + + public void setLoading() { + preview.setText(PantsBundle.message("pants.bsp.loading")); + } + + public void updatePreview (Set addresses) { + String newText = addresses.stream() + .map(PantsTargetAddress::toAddressString) + .sorted() + .collect(Collectors.joining("\n")) + "\n"; + preview.setText(newText); + } +} diff --git a/src/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressor.java b/src/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressor.java index 6e94a87bb..e8231d883 100644 --- a/src/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressor.java +++ b/src/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressor.java @@ -3,6 +3,7 @@ package com.twitter.intellij.pants.service.project.modifier; +import com.google.common.collect.Sets; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.io.FileUtil; import com.twitter.intellij.pants.service.PantsCompileOptionsExecutor; @@ -11,7 +12,7 @@ import com.twitter.intellij.pants.service.project.model.ProjectInfo; import com.twitter.intellij.pants.service.project.model.TargetInfo; import com.twitter.intellij.pants.util.PantsUtil; -import org.apache.commons.compress.utils.Sets; + import org.jetbrains.annotations.NotNull; import java.io.File; diff --git a/src/com/twitter/intellij/pants/ui/PantsToBspProjectAction.java b/src/com/twitter/intellij/pants/ui/PantsToBspProjectAction.java index 4fbed6321..fe4658e26 100644 --- a/src/com/twitter/intellij/pants/ui/PantsToBspProjectAction.java +++ b/src/com/twitter/intellij/pants/ui/PantsToBspProjectAction.java @@ -3,6 +3,7 @@ package com.twitter.intellij.pants.ui; +import com.google.common.collect.Streams; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.ide.impl.OpenProjectTask; import com.intellij.ide.impl.ProjectUtil; @@ -18,6 +19,7 @@ import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; +import com.twitter.intellij.pants.bsp.FastpassUtils; import com.twitter.intellij.pants.settings.PantsSettings; import com.twitter.intellij.pants.util.PantsUtil; import org.apache.commons.io.IOUtils; @@ -25,9 +27,7 @@ import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; @@ -116,34 +116,14 @@ private Optional existingProjectPath(String output, Path workspace) { } private GeneralCommandLine createCommandLine(Project project) throws IOException { - GeneralCommandLine commandLine = PantsUtil.defaultCommandLine(project); - - String coursier = coursierPath().toString(); - commandLine.setExePath(coursier); - - List commandBase = Arrays.asList( - "launch", "org.scalameta:metals_2.12:latest.stable", - "--main", "scala.meta.internal.pantsbuild.BloopPants", - "--", + List fastpassCommandBase = Arrays.asList( "create", "--intellij", "--intellijLauncher", "echo" ); - commandLine.addParameters(commandBase); - List targets = pantsTargets(project); - commandLine.addParameters(targets); - return commandLine; - } - - private Path coursierPath() throws IOException { - Path destination = Paths.get(System.getProperty("java.io.tmpdir"), "pants-plugin-coursier"); - if (!Files.exists(destination)) { - URL url = new URL("https://git.io/coursier-cli"); - Files.copy(url.openConnection().getInputStream(), destination); - destination.toFile().setExecutable(true); - } - return destination; + List fastpassCommand = Streams.concat(fastpassCommandBase.stream(), targets.stream()).collect(Collectors.toList()); + return FastpassUtils.makeFastpassCommand(project, fastpassCommand); } private List pantsTargets(Project project) { diff --git a/tests/com/twitter/intellij/pants/bsp/PantsTargetAddressTest.java b/tests/com/twitter/intellij/pants/bsp/PantsTargetAddressTest.java new file mode 100644 index 000000000..a3993045d --- /dev/null +++ b/tests/com/twitter/intellij/pants/bsp/PantsTargetAddressTest.java @@ -0,0 +1,36 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.bsp; + +import com.intellij.testFramework.UsefulTestCase; + +import java.nio.file.Paths; +import java.util.Optional; + +public class PantsTargetAddressTest extends UsefulTestCase { + public void testDirectEntry() { + Optional t = PantsTargetAddress.tryParse("project:target"); + assertEquals(t.get(), PantsTargetAddress.oneTargetInDir(Paths.get("project"), "target")); + } + + public void testRecursiveEntry() { + Optional t = PantsTargetAddress.tryParse("project::"); + assertEquals(t.get(), PantsTargetAddress.allTargetsInDirDeep(Paths.get("project"))); + } + + public void testFlatEntry() { + Optional t = PantsTargetAddress.tryParse("project:"); + assertEquals(t.get(), PantsTargetAddress.allTargetsInDirFlat(Paths.get("project"))); + } + + public void testJustColon() { + Optional t = PantsTargetAddress.tryParse(":"); + assertFalse(t.isPresent()); + } + + public void testJustDoubleColon() { + Optional t = PantsTargetAddress.tryParse("::"); + assertFalse(t.isPresent()); + } +} diff --git a/tests/com/twitter/intellij/pants/integration/BspPantsIntegrationTest.java b/tests/com/twitter/intellij/pants/integration/BspPantsIntegrationTest.java new file mode 100644 index 000000000..8e9bc0e9b --- /dev/null +++ b/tests/com/twitter/intellij/pants/integration/BspPantsIntegrationTest.java @@ -0,0 +1,85 @@ +// Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +// Licensed under the Apache License, Version 2.0 (see LICENSE). + +package com.twitter.intellij.pants.integration; + +import com.twitter.intellij.pants.bsp.FastpassTargetListCache; +import com.twitter.intellij.pants.bsp.FastpassUtils; +import com.twitter.intellij.pants.bsp.InvalidTargetException; +import com.twitter.intellij.pants.bsp.PantsTargetAddress; +import com.twitter.intellij.pants.testFramework.OSSPantsIntegrationTest; + +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionException; + +public class BspPantsIntegrationTest extends OSSPantsIntegrationTest { + FastpassTargetListCache cache; + + public void testFileSync() throws Throwable { + doImport("examples/tests/java/org/pantsbuild/example/hello"); + cache = new FastpassTargetListCache(Paths.get(myProjectRoot.getPath())); + + // Test flat directory rule + Map> parsedEmptyFlatDirectoryRule = + parse(Collections.singleton("examples/tests/java/org/pantsbuild/example/hello:")); + assertEquals(1, parsedEmptyFlatDirectoryRule.size()); + assertEquals(0, parsedEmptyFlatDirectoryRule.values().stream().findFirst().get().size()); + + // Test flat directory rule + Map> parsedNonEmptyFlatDirectoryRule = + parse(Collections.singleton("examples/tests/java/org/pantsbuild/example/hello/greet:")); + assertEquals(1, parsedNonEmptyFlatDirectoryRule.size()); + assertEquals(1, parsedNonEmptyFlatDirectoryRule.values().stream().findFirst().get().size()); + + // Test deep directory rule + Map> parsedDeepDirectoryRule = + parse(Collections.singleton("examples/tests/java/org/pantsbuild/example/hello::")); + assertEquals(1, parsedDeepDirectoryRule.size()); + assertEquals(1, parsedDeepDirectoryRule.values().stream().findFirst().get().size()); + + // Test single target rule + Map> parsedSingeTargetRule = + parse(Collections.singleton("examples/tests/java/org/pantsbuild/example/hello/greet:greet")); + assertEquals(1, parsedSingeTargetRule.size()); + assertEquals(1, parsedSingeTargetRule.values().stream().findFirst().get().size()); + + // Throws for malformed target + assertThrows( + InvalidTargetException.class, + "Malformed address", + () -> parse(Collections.singleton("examples/tests/java/org/pantsbuild/example/hello/greet")) + ); + + // Throws for non-existing folder + assertThrows( + InvalidTargetException.class, + "No such folder", + () -> parse(Collections.singleton("examples/tests/java/org/pantsbuild/example/hello/greet1:")) + ); + + // Throws for non-existing target + assertThrows( + InvalidTargetException.class, + "No such target", + () -> parse(Collections.singleton("examples/tests/java/org/pantsbuild/example/hello/greet:greet1")) + ); + } + + private Map> parse(Set rules) + throws Throwable { + try { + return FastpassUtils.validateAndGetPreview( + myProjectRoot, + rules, + cache::getTargetsList + ).join(); + } catch (CompletionException e) { + throw e.getCause(); + } + } +} + diff --git a/tests/com/twitter/intellij/pants/quickfix/AddPantsTargetDependencyFixTest.java b/tests/com/twitter/intellij/pants/quickfix/AddPantsTargetDependencyFixTest.java index 813a7772f..376769a7c 100644 --- a/tests/com/twitter/intellij/pants/quickfix/AddPantsTargetDependencyFixTest.java +++ b/tests/com/twitter/intellij/pants/quickfix/AddPantsTargetDependencyFixTest.java @@ -21,13 +21,7 @@ private void doTest(final String targetName, String addressToAdd) { final AddPantsTargetDependencyFix dependencyFix = new AddPantsTargetDependencyFix(address, dependencyAddress); WriteCommandAction.Simple.runWriteCommandAction( - getProject(), - new Runnable() { - @Override - public void run() { - dependencyFix.doInsert(myFixture.getFile(), targetName, dependencyAddress); - } - } + getProject(), () -> dependencyFix.doInsert(myFixture.getFile(), targetName, dependencyAddress) ); // FIXME: Fix the formatting of files under testData/quickfix/addPantsTargetDependency myFixture.checkResultByFile(testName + "_expected.py"); diff --git a/tests/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressorTest.java b/tests/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressorTest.java index 9504f126e..76742be45 100644 --- a/tests/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressorTest.java +++ b/tests/com/twitter/intellij/pants/service/project/modifier/PantsSourceRootCompressorTest.java @@ -3,8 +3,8 @@ package com.twitter.intellij.pants.service.project.modifier; +import com.google.common.collect.Sets; import junit.framework.TestCase; -import org.apache.commons.compress.utils.Sets; import java.io.File;