diff --git a/build.gradle b/build.gradle index a5a82af913..4bc890a67f 100644 --- a/build.gradle +++ b/build.gradle @@ -120,8 +120,8 @@ project.afterEvaluate { into perlUtils } copy { - from file("perl-utils/Devel") - into "$perlUtils/lib/" + from file("perl-utils/Devel/") + into "$perlUtils/lib/Devel/" } } } diff --git a/perl-utils/Devel/Cover/Report/Camelcade.pm b/perl-utils/Devel/Cover/Report/Camelcade.pm index 03b4f48988..9ab20bf3da 100644 --- a/perl-utils/Devel/Cover/Report/Camelcade.pm +++ b/perl-utils/Devel/Cover/Report/Camelcade.pm @@ -9,16 +9,22 @@ sub report { my $options = shift; my $report = $db->cover; - my $result = {}; + my $result = []; for my $file_name ($report->items) { + next unless ($file_name); + my $file_result = { + name => $file_name // "", + lines => {} + + }; + push @$result, $file_result; my $file_data = $report->file($file_name); for my $criterion_name ($file_data->items) { my $criterion = $file_data->criterion($criterion_name); if ($criterion_name eq 'statement') { for my $location_id ($criterion->items) { my $location_data = $criterion->location($location_id); - $result->{$file_name} //= {}; - my $location_result = $result->{$file_name}{$location_id} //= {}; + my $location_result = $file_result->{lines}{$location_id} //= {}; foreach my $element (@$location_data) { $location_result->{data}++; $location_result->{cover} += $element->covered // 0; @@ -29,8 +35,7 @@ sub report { elsif ($criterion_name eq 'time') { for my $location_id ($criterion->items) { my $location_data = $criterion->location($location_id); - $result->{$file_name} //= {}; - my $location_result = $result->{$file_name}{$location_id} //= {}; + my $location_result = $file_result->{lines}{$location_id} //= {}; foreach my $element (@$location_data) { $location_result->{time} += $element->covered // 0; } @@ -47,7 +52,7 @@ sub report { } } - print JSON->new()->encode($result); + print JSON->new()->pretty(0)->encode($result); } 1; \ No newline at end of file diff --git a/resources/messages/PerlBundle.properties b/resources/messages/PerlBundle.properties index 5d38e4acc8..60a39fafe5 100644 --- a/resources/messages/PerlBundle.properties +++ b/resources/messages/PerlBundle.properties @@ -260,6 +260,13 @@ perl.template.context.switch=inside switch block perl.template.context.switch.after.case=inside switch after case perl.template.context.catch=catch compound perl.template.context.continue=continue block - - +perl.select.sdk.notification=Perl5 Interpreter +perl.select.sdk.notification.title=Perl5 Interpreter is not Configured +perl.select.sdk.notification.message=To make it work you should select a Perl5 interpreter +perl.select.sdk.notification.action=To make it work you should select a Perl5 interpreter +perl.missing.library.notification=Perl5 Missing Library +perl.missing.library.notification.title=Perl5 Library {0} is Missing +perl.missing.library.notification.message=Library is require to perform an action. Please, install it first +perl.coverage.loading.error=Perl5 Coverage Loading Error +perl.configure.interpreter.action=Configure diff --git a/src/com/perl5/lang/perl/idea/coverage/PerlCoverageEngine.java b/src/com/perl5/lang/perl/idea/coverage/PerlCoverageEngine.java index ca7ad9583d..d19800482a 100644 --- a/src/com/perl5/lang/perl/idea/coverage/PerlCoverageEngine.java +++ b/src/com/perl5/lang/perl/idea/coverage/PerlCoverageEngine.java @@ -23,6 +23,7 @@ import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.perl5.lang.perl.fileTypes.PurePerlFileType; @@ -31,6 +32,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.File; import java.util.Collections; import java.util.Date; import java.util.List; @@ -116,7 +118,17 @@ public boolean recompileProjectAndRerunAction(@NotNull Module module, @NotNull @Override public Set getQualifiedNames(@NotNull PsiFile sourceFile) { - return Collections.emptySet(); // fixme investigate + return Collections.singleton(buildQualifiedName(sourceFile)); + } + + @Nullable + @Override + public String getQualifiedName(@NotNull File outputFile, @NotNull PsiFile sourceFile) { + return buildQualifiedName(sourceFile); + } + + private static String buildQualifiedName(@NotNull PsiFile sourceFile) { + return FileUtil.toSystemIndependentName(sourceFile.getVirtualFile().getPath()); } @Override diff --git a/src/com/perl5/lang/perl/idea/coverage/PerlCoverageRunner.java b/src/com/perl5/lang/perl/idea/coverage/PerlCoverageRunner.java index 23cec1011e..a93f6deb38 100644 --- a/src/com/perl5/lang/perl/idea/coverage/PerlCoverageRunner.java +++ b/src/com/perl5/lang/perl/idea/coverage/PerlCoverageRunner.java @@ -16,26 +16,152 @@ package com.perl5.lang.perl.idea.coverage; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; import com.intellij.coverage.CoverageEngine; import com.intellij.coverage.CoverageRunner; import com.intellij.coverage.CoverageSuite; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.process.ProcessOutput; +import com.intellij.execution.util.ExecUtil; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.rt.coverage.data.ClassData; +import com.intellij.rt.coverage.data.LineCoverage; +import com.intellij.rt.coverage.data.LineData; import com.intellij.rt.coverage.data.ProjectData; import com.perl5.PerlBundle; +import com.perl5.lang.perl.util.PerlPluginUtil; +import com.perl5.lang.perl.util.PerlRunUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; +import java.util.Map; +import java.util.Set; public class PerlCoverageRunner extends CoverageRunner { + private static final String COVER = "cover"; + private static final String COVER_LIB = "Devel::Cover"; + private static final Logger LOG = Logger.getInstance(PerlCoverageRunner.class); + @Override public ProjectData loadCoverageData(@NotNull File sessionDataFile, @Nullable CoverageSuite baseCoverageSuite) { if (!(baseCoverageSuite instanceof PerlCoverageSuite)) { return null; } - // todo implement loading + Project project = baseCoverageSuite.getProject(); + VirtualFile coverFile = PerlRunUtil.findLibraryScriptWithNotification(project, COVER, COVER_LIB); + if (coverFile == null) { + return null; + } + + String libRoot = PerlPluginUtil.getPluginPerlLibRoot(); + if (libRoot == null) { + return null; + } + + GeneralCommandLine perlCommandLine = PerlRunUtil.getPerlCommandLine(project, coverFile, + "-I" + FileUtil.toSystemIndependentName(libRoot)); + if (perlCommandLine == null) { + return null; // fixme should be a notification + } + + perlCommandLine.addParameters( + "--silent", "--nosummary", "-report", "camelcade", sessionDataFile.getAbsolutePath() + ); + + try { + LOG.info("Loading coverage by: " + perlCommandLine.getCommandLineString()); + ProcessOutput output = ExecUtil.execAndGetOutput(perlCommandLine); + if (output.getExitCode() != 0) { + String errorMessage = output.getStderr(); + if (StringUtil.isEmpty(errorMessage)) { + errorMessage = output.getStdout(); + } + + if (!StringUtil.isEmpty(errorMessage)) { + showError(project, errorMessage); + } + return null; + } + String stdout = output.getStdout(); + if (StringUtil.isEmpty(stdout)) { + return null; + } + + try { + PerlFileData[] filesData = new Gson().fromJson(stdout, PerlFileData[].class); + if (filesData != null) { + return parsePerlFileData(filesData); + } + } + catch (JsonParseException e) { + showError(project, e.getMessage()); + LOG.warn("Error parsing JSON", e); + } + } + catch (ExecutionException e) { + showError(project, e.getMessage()); + LOG.warn("Error loading coverage", e); + } return null; } + private static ProjectData parsePerlFileData(@NotNull PerlFileData[] filesData) { + ProjectData projectData = new ProjectData(); + for (PerlFileData perlFileData : filesData) { + if (StringUtil.isEmpty(perlFileData.name) || perlFileData.lines == null) { + continue; + } + ClassData classData = projectData.getOrCreateClassData(FileUtil.toSystemIndependentName(perlFileData.name)); + Set> linesEntries = perlFileData.lines.entrySet(); + Integer maxLineNumber = linesEntries.stream().map(Map.Entry::getKey).max(Integer::compare).orElse(0); + LineData[] linesData = new LineData[maxLineNumber + 1]; + for (Map.Entry lineEntry : linesEntries) { + PerlLineData perlLineData = lineEntry.getValue(); + final Integer lineNumber = lineEntry.getKey(); + LineData lineData = new LineData(lineNumber, null) { + @Override + public int getStatus() { + if (perlLineData.cover == 0) { + return LineCoverage.NONE; + } + else if (perlLineData.cover < perlLineData.data) { + return LineCoverage.PARTIAL; + } + return LineCoverage.FULL; + } + }; + lineData.setHits(perlLineData.cover); + linesData[lineNumber] = lineData; + } + + classData.setLines(linesData); + } + + return projectData; + } + + private static void showError(@NotNull Project project, @NotNull String message) { + Notifications.Bus.notify( + new Notification( + PerlBundle.message("perl.coverage.loading.error"), + PerlBundle.message("perl.coverage.loading.error"), + message, + NotificationType.ERROR + ), + project + ); + } + @Override public String getPresentableName() { return PerlBundle.message("perl.perl5"); diff --git a/src/com/perl5/lang/perl/idea/coverage/PerlFileData.java b/src/com/perl5/lang/perl/idea/coverage/PerlFileData.java new file mode 100644 index 0000000000..3f2877be1f --- /dev/null +++ b/src/com/perl5/lang/perl/idea/coverage/PerlFileData.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015-2017 Alexandr Evstigneev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.perl5.lang.perl.idea.coverage; + +import java.util.Map; + +class PerlFileData { + public String name; + public Map lines; +} diff --git a/src/com/perl5/lang/perl/idea/coverage/PerlLineData.java b/src/com/perl5/lang/perl/idea/coverage/PerlLineData.java new file mode 100644 index 0000000000..22ee875dd0 --- /dev/null +++ b/src/com/perl5/lang/perl/idea/coverage/PerlLineData.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015-2017 Alexandr Evstigneev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.perl5.lang.perl.idea.coverage; + +class PerlLineData { + int data; + int cover; + int uncoverable; + int time; +} diff --git a/src/com/perl5/lang/perl/idea/project/PerlProjectManager.java b/src/com/perl5/lang/perl/idea/project/PerlProjectManager.java index 00b5eab86d..858281a3f4 100644 --- a/src/com/perl5/lang/perl/idea/project/PerlProjectManager.java +++ b/src/com/perl5/lang/perl/idea/project/PerlProjectManager.java @@ -16,6 +16,10 @@ package com.perl5.lang.perl.idea.project; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.application.WriteAction; @@ -23,6 +27,7 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.ProjectJdkTable; import com.intellij.openapi.projectRoots.Sdk; @@ -42,7 +47,9 @@ import com.intellij.util.containers.ContainerUtil.ImmutableMapBuilder; import com.intellij.util.containers.FactoryMap; import com.intellij.util.messages.MessageBusConnection; +import com.perl5.PerlBundle; import com.perl5.lang.perl.idea.configuration.settings.PerlLocalSettings; +import com.perl5.lang.perl.idea.configuration.settings.sdk.Perl5SettingsConfigurable; import com.perl5.lang.perl.idea.configuration.settings.sdk.PerlSdkLibrary; import com.perl5.lang.perl.idea.modules.PerlLibrarySourceRootType; import com.perl5.lang.perl.idea.modules.PerlSourceRootType; @@ -248,6 +255,35 @@ public static Sdk getSdk(@Nullable Module module) { return getInstance(module.getProject()).getProjectSdk(); } + /** + * @return sdk for project. If not configured - suggests to configure + */ + public static Sdk getSdkWithNotification(@NotNull Project project) { + Sdk sdk = getSdk(project); + if (sdk != null) { + return sdk; + } + showUnconfiguredInterpreterNotification(project); + return null; + } + + public static void showUnconfiguredInterpreterNotification(@NotNull Project project) { + Notification notification = new Notification( + PerlBundle.message("perl.select.sdk.notification"), + PerlBundle.message("perl.select.sdk.notification.title"), + PerlBundle.message("perl.select.sdk.notification.message"), + NotificationType.ERROR + ); + notification.addAction(new DumbAwareAction(PerlBundle.message("perl.configure.interpreter.action")) { + @Override + public void actionPerformed(AnActionEvent e) { + Perl5SettingsConfigurable.open(project); + notification.expire(); + } + }); + Notifications.Bus.notify(notification, project); + } + @Nullable public static Sdk getSdk(@Nullable Project project) { if (project == null) { diff --git a/src/com/perl5/lang/perl/idea/sdk/PerlSdkType.java b/src/com/perl5/lang/perl/idea/sdk/PerlSdkType.java index 90a002afba..976fdef026 100644 --- a/src/com/perl5/lang/perl/idea/sdk/PerlSdkType.java +++ b/src/com/perl5/lang/perl/idea/sdk/PerlSdkType.java @@ -74,7 +74,7 @@ public void setupSdkPaths(@NotNull Sdk sdk) { public List getINCPaths(String sdkHomePath) { String executablePath = getExecutablePath(sdkHomePath); List perlLibPaths = new ArrayList<>(); - for (String path : PerlRunUtil.getDataFromProgram( + for (String path : PerlRunUtil.getOutputFromProgram( executablePath, "-le", "print for @INC" @@ -173,7 +173,7 @@ public String getVersionString(@NotNull Sdk sdk) { @Nullable private VersionDescriptor getPerlVersionDescriptor(@NotNull String sdkHomePath) { - List versionLines = PerlRunUtil.getDataFromProgram(getExecutablePath(sdkHomePath), "-v"); + List versionLines = PerlRunUtil.getOutputFromProgram(getExecutablePath(sdkHomePath), "-v"); if (versionLines.isEmpty()) { return null; diff --git a/src/com/perl5/lang/perl/util/PerlPluginUtil.java b/src/com/perl5/lang/perl/util/PerlPluginUtil.java index ff26d6b470..4414c6ce8d 100644 --- a/src/com/perl5/lang/perl/util/PerlPluginUtil.java +++ b/src/com/perl5/lang/perl/util/PerlPluginUtil.java @@ -67,6 +67,15 @@ public static String getPluginPerlScriptsRoot() { return pluginRoot == null ? null : pluginRoot + "/perl"; } + /** + * @return path to libraries root shipped with plugins to use in -I + */ + @Nullable + public static String getPluginPerlLibRoot() { + String perlScriptsRoot = getPluginPerlScriptsRoot(); + return perlScriptsRoot == null ? null : FileUtil.join(perlScriptsRoot, "lib"); + } + @Nullable public static VirtualFile getPluginScriptVirtualFile(String scriptName) { String scriptsRoot = getPluginPerlScriptsRoot(); @@ -79,10 +88,10 @@ public static VirtualFile getPluginScriptVirtualFile(String scriptName) { } @Nullable - public static GeneralCommandLine getPluginScriptCommandLine(Project project, String script, String... params) { + public static GeneralCommandLine getPluginScriptCommandLine(Project project, String script, String... perlParams) { VirtualFile scriptVirtualFile = getPluginScriptVirtualFile(script); if (scriptVirtualFile != null) { - return PerlRunUtil.getPerlCommandLine(project, scriptVirtualFile, params); + return PerlRunUtil.getPerlCommandLine(project, scriptVirtualFile, perlParams); } return null; } diff --git a/src/com/perl5/lang/perl/util/PerlRunUtil.java b/src/com/perl5/lang/perl/util/PerlRunUtil.java index 6334440719..1c63193652 100644 --- a/src/com/perl5/lang/perl/util/PerlRunUtil.java +++ b/src/com/perl5/lang/perl/util/PerlRunUtil.java @@ -18,16 +18,27 @@ import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.util.ExecUtil; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.projectRoots.SdkTypeId; +import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; +import com.perl5.PerlBundle; import com.perl5.lang.perl.idea.project.PerlProjectManager; import com.perl5.lang.perl.idea.sdk.PerlSdkType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Created by hurricup on 26.04.2016. @@ -61,10 +72,120 @@ public static GeneralCommandLine getPerlCommandLine(@NotNull Project project, return commandLine; } + /** + * Attempts to find a script in project's perl sdk and shows notification to user with suggestion to install a library if + * script was not found + * + * @param project to get sdk from + * @param scriptName script name + * @param libraryName library to suggest if script was not found; notification won't be shown if lib is null/empty + * @return script's virtual file if any + */ + @Nullable + public static VirtualFile findLibraryScriptWithNotification(@NotNull Project project, + @NotNull String scriptName, + @Nullable String libraryName) { + Sdk sdk = PerlProjectManager.getSdkWithNotification(project); + if (sdk == null) { + return null; + } + return findLibraryScriptWithNotification( + sdk, + scriptName, + libraryName + ); + } + + + /** + * Attempts to find a script in project's perl sdk and shows notification to user with suggestion to install a library if + * script was not found + * + * @param sdk to find script in + * @param scriptName script name + * @param libraryName library to suggest if script was not found, notification won't be shown if lib is null/empty + * @return script's virtual file if any + */ + @Nullable + public static VirtualFile findLibraryScriptWithNotification(@NotNull Sdk sdk, + @NotNull String scriptName, + @Nullable String libraryName) { + VirtualFile scriptFile = findScript(sdk, scriptName); + if (scriptFile != null) { + return scriptFile; + } + + if (StringUtil.isEmpty(libraryName)) { + return null; + } + + Notification notification = new Notification( + PerlBundle.message("perl.missing.library.notification"), + PerlBundle.message("perl.missing.library.notification.title", libraryName), + PerlBundle.message("perl.missing.library.notification.message"), + NotificationType.ERROR + ); + // fixme add installation action here, see #1645 + Notifications.Bus.notify(notification); + + return null; + } + + /** + * Attempts to find a script by name in perl's libraries path + * + * @param project project to get sdk from + * @param scriptName script name to find + * @return script's virtual file if available + **/ + @Nullable + public static VirtualFile findScript(@Nullable Project project, @Nullable String scriptName) { + return findScript(PerlProjectManager.getSdk(project), scriptName); + } + + /** + * Attempts to find a script by name in perl's libraries path + * + * @param sdk perl sdk to search in + * @param scriptName script name to find + * @return script's virtual file if available + **/ + @Nullable + public static VirtualFile findScript(@Nullable Sdk sdk, @Nullable String scriptName) { + if (sdk == null || scriptName == null) { + return null; + } + ApplicationManager.getApplication().assertReadAccessAllowed(); + return getBinDirectories(sdk).stream().map(root -> root.findChild(scriptName)).filter(Objects::nonNull).findFirst().orElse(null); + } + + + /** + * @return list of perl bin directories where script from library may be located + **/ + @NotNull + public static List getBinDirectories(@NotNull Sdk sdk) { + ApplicationManager.getApplication().assertReadAccessAllowed(); + SdkTypeId sdkType = sdk.getSdkType(); + if (!(sdkType instanceof PerlSdkType)) { + throw new IllegalArgumentException("Got non-perl sdk: " + sdk); + } + VirtualFile[] roots = sdk.getRootProvider().getFiles(OrderRootType.CLASSES); + List result = new ArrayList<>(); + for (VirtualFile root : roots) { + if (root.isValid()) { + VirtualFile binDir = root.findFileByRelativePath("../bin"); + if (binDir != null && binDir.isValid() && binDir.isDirectory()) { + result.add(binDir); + } + } + } + return result; + } @Nullable public static String getPathFromPerl() { - List perlPathLines = getDataFromProgram("perl", "-le", "print $^X"); + List perlPathLines = getOutputFromProgram("perl", "-le", "print $^X"); if (perlPathLines.size() == 1) { int perlIndex = perlPathLines.get(0).lastIndexOf("perl"); @@ -76,7 +197,7 @@ public static String getPathFromPerl() { } @NotNull - public static List getDataFromProgram(String... command) { + public static List getOutputFromProgram(String... command) { try { GeneralCommandLine commandLine = new GeneralCommandLine(command); return ExecUtil.execAndGetOutput(commandLine).getStdoutLines();