├── .idea ├── .name ├── vcs.xml ├── compiler.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── modules.xml ├── misc.xml ├── gradle.xml ├── jarRepositories.xml ├── libraries-with-intellij-classes.xml └── uiDesigner.xml ├── gradle.properties ├── settings.gradle ├── src └── main │ ├── resources │ ├── META-INF │ │ ├── dummy.xml │ │ ├── plugin.xml │ │ ├── pluginIcon.svg │ │ └── pluginIcon_dark.svg │ └── icons │ │ ├── CoverageRunAction.svg │ │ ├── CoverageRunAction_dark.svg │ │ ├── TemplateLineMarker.svg │ │ └── TemplateLineMarker_dark.svg │ ├── kotlin │ ├── icons │ │ └── CPPCoverageIcons.kt │ └── net │ │ └── zero9178 │ │ └── cov │ │ ├── ProjectSemaphore.kt │ │ ├── util │ │ ├── ComparablePair.kt │ │ └── CTestCompatibility.kt │ │ ├── window │ │ ├── CoverageWindowFactory.kt │ │ ├── SettingsWindowImpl.kt │ │ └── CoverageViewImpl.kt │ │ ├── editor │ │ ├── CoverageFileAccessProtector.kt │ │ ├── CoverageTemplateLineMarkerProvider.kt │ │ └── CoverageHighlighter.kt │ │ ├── data │ │ ├── CoverageData.kt │ │ ├── CoverageGenerator.kt │ │ └── GCCGCDACoverageGenerator.kt │ │ ├── actions │ │ └── RunCoverageAction.kt │ │ ├── settings │ │ └── CoverageGeneratorSettings.kt │ │ └── CoverageConfigurationExtension.kt │ └── java │ └── net │ └── zero9178 │ └── cov │ └── window │ ├── CoverageView.java │ ├── SettingsWindow.java │ ├── CoverageView.form │ └── SettingsWindow.form ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── LICENSE ├── gradlew.bat ├── gradlew └── README.md /.idea/.name: -------------------------------------------------------------------------------- 1 | C C++ Coverage -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'C C++ Coverage' 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/dummy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zero9178/C-Cpp-Coverage-for-CLion/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/kotlin/icons/CPPCoverageIcons.kt: -------------------------------------------------------------------------------- 1 | package icons 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | 5 | object CPPCoverageIcons { 6 | @JvmField 7 | val COVERAGE_RUN_ACTION = IconLoader.getIcon("/icons/CoverageRunAction.svg", javaClass) 8 | 9 | val TEMPLATE_LINE_MARKER = IconLoader.getIcon("/icons/TemplateLineMarker.svg", javaClass) 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /.idea/shelf/ 3 | /.idea/workspace.xml 4 | 5 | # Datasource local storage ignored files 6 | /.idea/dataSources/ 7 | dataSources.local.xml 8 | 9 | # Editor-based HTTP Client requests 10 | /.idea/httpRequests/ 11 | rest-client.private.env.json 12 | http-client.private.env.json 13 | 14 | /.gradle 15 | # Project exclude paths 16 | /build/ 17 | /build/classes/java/main/ -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/ProjectSemaphore.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov 2 | 3 | import com.intellij.openapi.components.service 4 | import com.intellij.openapi.project.Project 5 | import java.util.concurrent.Semaphore 6 | 7 | class ProjectSemaphore { 8 | val semaphore = Semaphore(1, true) 9 | 10 | companion object { 11 | fun getInstance(project: Project) = project.service() 12 | } 13 | } -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/util/ComparablePair.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.util 2 | 3 | data class ComparablePair, B : Comparable>(val first: A, val second: B) : 4 | Comparable> { 5 | 6 | override fun compareTo(other: ComparablePair): Int { 7 | val result = first.compareTo(other.first) 8 | return if (result != 0) { 9 | result 10 | } else { 11 | second.compareTo(other.second) 12 | } 13 | } 14 | 15 | override fun toString() = "($first,$second)" 16 | 17 | fun toPair() = first to second 18 | } 19 | 20 | infix fun , B : Comparable> A.toCP(that: B) = ComparablePair(this, that) -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/window/CoverageWindowFactory.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.window 2 | 3 | import com.intellij.openapi.project.DumbAware 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.wm.ToolWindow 6 | import com.intellij.openapi.wm.ToolWindowFactory 7 | import com.intellij.ui.content.ContentFactory 8 | 9 | class CoverageWindowFactory : ToolWindowFactory, DumbAware { 10 | override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { 11 | 12 | val coverageView = CoverageView.getInstance(project) 13 | toolWindow.contentManager.addContent( 14 | ContentFactory.SERVICE.getInstance().createContent( 15 | coverageView.panel, 16 | "", 17 | false 18 | ) 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/editor/CoverageFileAccessProtector.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.editor 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import com.intellij.openapi.vfs.WritingAccessProvider 6 | 7 | class CoverageFileAccessProtector(val project: Project) : WritingAccessProvider() { 8 | 9 | companion object { 10 | val currentlyInCoverage = mutableMapOf>() 11 | } 12 | 13 | override fun getReadOnlyMessage(): String { 14 | return "Files cannot be edited while gathering coverage" 15 | } 16 | 17 | override fun requestWriting(files: MutableCollection): MutableCollection { 18 | val inCoverage = currentlyInCoverage[project] ?: return mutableListOf() 19 | return files.filter { 20 | inCoverage.contains(it) 21 | }.toMutableList() 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/net/zero9178/cov/window/CoverageView.java: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.window; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.ui.components.JBCheckBox; 5 | import com.intellij.ui.dualView.TreeTableView; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import javax.swing.*; 10 | import javax.swing.tree.DefaultMutableTreeNode; 11 | 12 | public abstract class CoverageView { 13 | 14 | protected TreeTableView myTreeTableView; 15 | protected JButton myClear; 16 | protected JBCheckBox myIncludeNonProjectSources; 17 | private JPanel myPanel; 18 | 19 | @NotNull 20 | public static CoverageView getInstance(@NotNull Project project) { 21 | return project.getService(CoverageView.class); 22 | } 23 | 24 | @NotNull 25 | public JPanel getPanel() { 26 | return myPanel; 27 | } 28 | 29 | protected abstract void createUIComponents(); 30 | 31 | public abstract void setRoot(@Nullable DefaultMutableTreeNode treeNode, boolean hasBranchCoverage, boolean hasExternalSources); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 zero9178 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/data/CoverageData.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.data 2 | 3 | import net.zero9178.cov.util.ComparablePair 4 | 5 | //All line and column numbers in the file start with 1 and 1 6 | data class CoverageData( 7 | val files: Map, 8 | val hasBranchCoverage: Boolean, 9 | val containsExternalSources: Boolean 10 | ) 11 | 12 | data class CoverageFileData(val filePath: String, val functions: Map) 13 | 14 | sealed class FunctionCoverageData { 15 | abstract val data: Any 16 | } 17 | 18 | class FunctionLineData(override val data: Map) : FunctionCoverageData() 19 | 20 | class FunctionRegionData(override val data: List) : FunctionCoverageData() { 21 | data class Region( 22 | val startPos: ComparablePair, 23 | val endPos: ComparablePair, 24 | val executionCount: Long, 25 | val kind: Kind 26 | ) { 27 | enum class Kind { 28 | Code, Gap, Skipped, Expanded 29 | } 30 | } 31 | } 32 | 33 | data class CoverageFunctionData( 34 | val startPos: ComparablePair, 35 | val endPos: ComparablePair, 36 | val functionName: String, 37 | val coverage: FunctionCoverageData, 38 | val branches: List 39 | ) 40 | 41 | data class CoverageBranchData( 42 | val startPos: ComparablePair, 43 | val steppedInCount: Int, 44 | val skippedCount: Int 45 | ) -------------------------------------------------------------------------------- /src/main/java/net/zero9178/cov/window/SettingsWindow.java: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.window; 2 | 3 | import com.intellij.openapi.options.Configurable; 4 | import com.intellij.openapi.ui.ComboBox; 5 | import com.intellij.openapi.ui.TextFieldWithBrowseButton; 6 | import com.intellij.ui.AnimatedIcon; 7 | import com.intellij.ui.HyperlinkLabel; 8 | import com.intellij.ui.components.JBCheckBox; 9 | import com.intellij.ui.components.JBLabel; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import javax.swing.*; 13 | 14 | public abstract class SettingsWindow implements Configurable { 15 | 16 | protected ComboBox myComboBox; 17 | protected TextFieldWithBrowseButton myGcovOrllvmCovBrowser; 18 | protected TextFieldWithBrowseButton myLLVMProfdataBrowser; 19 | protected com.intellij.ui.components.JBLabel myGcovOrLLVMCovLabel; 20 | protected com.intellij.ui.components.JBLabel myLLVMProfLabel; 21 | protected JBLabel myErrors; 22 | protected JBCheckBox myIfBranchCoverage; 23 | protected JBCheckBox myLoopBranchCoverage; 24 | protected JBCheckBox myBooleanOpBranchCoverage; 25 | protected JLabel myDemanglerLabel; 26 | protected TextFieldWithBrowseButton myDemanglerBrowser; 27 | private JPanel myPanel; 28 | private JBLabel myLoading; 29 | protected JCheckBox myDoBranchCoverage; 30 | protected JBCheckBox myCondBranchCoverage; 31 | protected JCheckBox myCalculateExternal; 32 | protected HyperlinkLabel myDocHyperlink; 33 | 34 | protected void setLoading(boolean loading) { 35 | myLoading.setIcon(loading ? new AnimatedIcon.Default() : null); 36 | } 37 | 38 | @Override 39 | public @NotNull 40 | JComponent createComponent() { 41 | return myPanel; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/util/CTestCompatibility.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.util 2 | 3 | import com.intellij.ide.plugins.PluginManager 4 | import com.intellij.openapi.extensions.PluginId 5 | import com.intellij.openapi.util.SystemInfo 6 | import com.jetbrains.cidr.cpp.cmake.model.CMakeConfiguration 7 | import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace 8 | import com.jetbrains.cidr.cpp.execution.CMakeAppRunConfiguration 9 | import com.jetbrains.cidr.cpp.execution.CMakeBuildProfileExecutionTarget 10 | import com.jetbrains.cidr.cpp.execution.testing.CMakeTestRunConfiguration 11 | import com.jetbrains.cidr.cpp.execution.testing.ctest.CidrCTestRunConfigurationData 12 | 13 | fun isCTestInstalled() = 14 | PluginManager.getInstance().findEnabledPlugin(PluginId.getId("org.jetbrains.plugins.clion.ctest")) != null 15 | 16 | fun getCMakeConfigurations( 17 | configuration: CMakeAppRunConfiguration, 18 | executionTarget: CMakeBuildProfileExecutionTarget 19 | ): Sequence { 20 | return if (configuration is CMakeTestRunConfiguration && isCTestInstalled() && configuration.testData is CidrCTestRunConfigurationData) { 21 | val testData = configuration.testData as CidrCTestRunConfigurationData 22 | testData.testListCopy?.mapNotNull { 23 | it?.command?.exePath 24 | }?.distinct()?.asSequence()?.mapNotNull { executable -> 25 | CMakeWorkspace.getInstance(configuration.project).modelTargets.asSequence().mapNotNull { target -> 26 | target.buildConfigurations.find { it.profileName == executionTarget.profileName } 27 | }.find { 28 | it.productFile?.name == executable.substringAfterLast( 29 | '/', 30 | if (SystemInfo.isWindows) executable.substringAfterLast('\\') else executable 31 | ) 32 | } 33 | } ?: emptySequence() 34 | } else { 35 | val runConfiguration = 36 | configuration.getBuildAndRunConfigurations(executionTarget)?.buildConfiguration 37 | ?: return emptySequence() 38 | sequenceOf(runConfiguration) 39 | } 40 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/java/net/zero9178/cov/window/CoverageView.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | -------------------------------------------------------------------------------- /.idea/libraries-with-intellij-classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 56 | 57 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/actions/RunCoverageAction.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.actions 2 | 3 | import com.intellij.execution.ExecutionManager 4 | import com.intellij.execution.ExecutionTargetManager 5 | import com.intellij.execution.RunManager 6 | import com.intellij.execution.configurations.RunnerSettings 7 | import com.intellij.execution.executors.DefaultRunExecutor 8 | import com.intellij.execution.runners.ExecutionEnvironmentBuilder 9 | import com.intellij.ide.macro.MacroManager 10 | import com.intellij.openapi.actionSystem.AnActionEvent 11 | import com.intellij.openapi.project.DumbAwareAction 12 | import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace 13 | import com.jetbrains.cidr.cpp.execution.CMakeAppRunConfiguration 14 | import com.jetbrains.cidr.cpp.execution.CMakeBuildProfileExecutionTarget 15 | import com.jetbrains.cidr.cpp.toolchains.CPPToolchains 16 | import net.zero9178.cov.settings.CoverageGeneratorSettings 17 | import org.jdom.Element 18 | 19 | class RunCoverageSettings(val executionTarget: CMakeBuildProfileExecutionTarget) : RunnerSettings { 20 | override fun readExternal(element: Element?) { 21 | 22 | } 23 | 24 | override fun writeExternal(element: Element?) { 25 | 26 | } 27 | } 28 | 29 | class CoverageButton : DumbAwareAction() { 30 | 31 | override fun update(e: AnActionEvent) { 32 | val project = e.project 33 | if (project == null) { 34 | e.presentation.isEnabled = false 35 | return 36 | } 37 | e.presentation.isEnabled = false 38 | val manager = RunManager.getInstance(project) 39 | val settings = manager.selectedConfiguration ?: return 40 | e.presentation.text = "Run '${settings.name}' with C/C++ Coverage Plugin" 41 | 42 | if (ExecutionTargetManager.getActiveTarget(project) !is CMakeBuildProfileExecutionTarget) { 43 | return 44 | } 45 | 46 | val runConfig = CMakeAppRunConfiguration.getSelectedRunConfiguration(project) ?: return 47 | val cmakeConfig = CMakeWorkspace.getInstance(project).getCMakeConfigurationFor( 48 | runConfig.getResolveConfiguration( 49 | ExecutionTargetManager.getInstance(project).activeTarget 50 | ) 51 | ) ?: return 52 | val info = CMakeWorkspace.getInstance(project).getProfileInfoFor(cmakeConfig) 53 | val toolchain = info.profile.toolchainName ?: CPPToolchains.getInstance().defaultToolchain?.name ?: return 54 | val gen = CoverageGeneratorSettings.getInstance() 55 | .getGeneratorFor(toolchain) 56 | e.presentation.isEnabled = gen?.first != null 57 | } 58 | 59 | override fun actionPerformed(anActionEvent: AnActionEvent) { 60 | val executor = DefaultRunExecutor.getRunExecutorInstance() ?: return 61 | val project = anActionEvent.project ?: return 62 | val manager = RunManager.getInstance(project) 63 | val settings = manager.selectedConfiguration ?: return 64 | 65 | val config = settings.configuration as? CMakeAppRunConfiguration ?: return 66 | 67 | MacroManager.getInstance().cacheMacrosPreview(anActionEvent.dataContext) 68 | val envBuilder = ExecutionEnvironmentBuilder.create(executor, settings) 69 | 70 | val executionTarget = 71 | ExecutionTargetManager.getActiveTarget(project) as? CMakeBuildProfileExecutionTarget ?: return 72 | val environment = envBuilder.run { 73 | dataContext(anActionEvent.dataContext) 74 | activeTarget() 75 | runnerSettings(RunCoverageSettings(executionTarget)) 76 | }.build() 77 | 78 | ExecutionManager.getInstance(config.project).restartRunProfile(environment) 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/data/CoverageGenerator.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.data 2 | 3 | import com.intellij.execution.configurations.GeneralCommandLine 4 | import com.intellij.execution.process.CapturingProcessHandler 5 | import com.intellij.execution.wsl.WSLCommandLineOptions 6 | import com.intellij.openapi.progress.ProgressIndicator 7 | import com.intellij.util.io.exists 8 | import com.jetbrains.cidr.cpp.execution.CMakeAppRunConfiguration 9 | import com.jetbrains.cidr.cpp.execution.CMakeBuildProfileExecutionTarget 10 | import com.jetbrains.cidr.cpp.toolchains.CPPEnvironment 11 | import com.jetbrains.cidr.cpp.toolchains.WSL 12 | import com.jetbrains.cidr.execution.ConfigurationExtensionContext 13 | import java.nio.file.Paths 14 | 15 | interface CoverageGenerator { 16 | fun patchEnvironment( 17 | configuration: CMakeAppRunConfiguration, 18 | environment: CPPEnvironment, 19 | executionTarget: CMakeBuildProfileExecutionTarget, 20 | cmdLine: GeneralCommandLine, 21 | context: ConfigurationExtensionContext 22 | ) { 23 | } 24 | 25 | fun generateCoverage( 26 | configuration: CMakeAppRunConfiguration, 27 | environment: CPPEnvironment, 28 | executionTarget: CMakeBuildProfileExecutionTarget, 29 | indicator: ProgressIndicator, 30 | context: ConfigurationExtensionContext 31 | ): CoverageData? { 32 | return null 33 | } 34 | } 35 | 36 | private fun extractVersion(line: String): Triple { 37 | val result = "(\\d+)\\.(\\d+)\\.(\\d+)".toRegex().findAll(line).lastOrNull() ?: return Triple(0, 0, 0) 38 | val (first, second, value) = result.destructured 39 | return Triple(first.toInt(), second.toInt(), value.toInt()) 40 | } 41 | 42 | fun createGeneratorFor( 43 | executable: String, 44 | maybeOptionalLLVMProf: String?, 45 | optionalDemangler: String?, 46 | wsl: WSL? 47 | ): Pair { 48 | if (executable.isBlank()) { 49 | return null to "No executable specified" 50 | } 51 | if (if (wsl == null) !Paths.get(executable).exists() else false) { 52 | return null to "Executable does not exist" 53 | } 54 | 55 | val commandLine = GeneralCommandLine(executable, "--version") 56 | if (wsl != null) { 57 | wsl.wslDistribution?.patchCommandLine(commandLine, null, WSLCommandLineOptions()) 58 | } 59 | 60 | val output = CapturingProcessHandler(commandLine).runProcess(5000) 61 | if (output.isTimeout) { 62 | return null to "Executable timed out" 63 | } 64 | 65 | val retCode = output.exitCode 66 | if (retCode != 0) { 67 | val stderrOutput = output.getStderrLines(false) 68 | return null to "Executable returned with error code $retCode and error output:\n ${stderrOutput.joinToString("\n")}" 69 | } 70 | val lines = output.getStdoutLines(false) 71 | when { 72 | lines[0].contains("LLVM", true) -> { 73 | if (maybeOptionalLLVMProf == null) { 74 | return null to "No llvm-profdata specified to accompany llvm-cov" 75 | } 76 | val majorVersion = extractVersion(lines[1]).first 77 | return LLVMCoverageGenerator(majorVersion, executable, maybeOptionalLLVMProf, optionalDemangler) to null 78 | } 79 | lines[0].contains("gcov", true) -> { 80 | val version = extractVersion(lines[0]) 81 | return if (version.first >= 9) { 82 | GCCJSONCoverageGenerator(executable, version.first) to null 83 | } else { 84 | GCCGCDACoverageGenerator(executable, version.first) to null 85 | } 86 | } 87 | else -> 88 | return null to "Executable identified as neither gcov nor llvm-cov" 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.zero9178 3 | C/C++ Coverage 4 | 5 | 6 | 7 | 8 | 9 | 11 | 14 | 15 | 17 |
    18 |
  • EAP Build
  • 19 |
20 | ]]>
21 | 22 | 27 | For further details, known issues and differences of compilers see
https://github.com/zero9178/C-Cpp-Coverage-for-CLion/blob/master/README.md 28 | ]]> 29 | 30 | com.intellij.modules.clion 31 | com.intellij.modules.lang 32 | org.jetbrains.plugins.clion.ctest 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 56 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/resources/icons/CoverageRunAction.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 29 | 49 | 53 | 57 | 61 | 62 | 65 | 70 | 75 | 80 | 82 | 87 | 88 | 90 | 94 | 95 | 97 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/main/resources/icons/CoverageRunAction_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 51 | 56 | 61 | 64 | 69 | 74 | 79 | 81 | 86 | 87 | 89 | 93 | 94 | 96 | 100 | 101 | 102 | 109 | 110 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/editor/CoverageTemplateLineMarkerProvider.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.editor 2 | 3 | import com.intellij.codeInsight.daemon.LineMarkerInfo 4 | import com.intellij.codeInsight.daemon.LineMarkerProvider 5 | import com.intellij.openapi.actionSystem.AnAction 6 | import com.intellij.openapi.actionSystem.AnActionEvent 7 | import com.intellij.openapi.actionSystem.CommonDataKeys 8 | import com.intellij.openapi.editor.ex.EditorEx 9 | import com.intellij.openapi.editor.markup.GutterIconRenderer 10 | import com.intellij.openapi.ui.ComboBox 11 | import com.intellij.openapi.ui.popup.Balloon 12 | import com.intellij.openapi.ui.popup.JBPopupFactory 13 | import com.intellij.openapi.util.Disposer 14 | import com.intellij.openapi.util.Key 15 | import com.intellij.psi.PsiDocumentManager 16 | import com.intellij.psi.PsiElement 17 | import com.intellij.psi.impl.source.tree.LeafPsiElement 18 | import com.intellij.psi.util.parents 19 | import com.intellij.ui.awt.RelativePoint 20 | import com.jetbrains.cidr.lang.parser.OCTokenTypes 21 | import com.jetbrains.cidr.lang.psi.OCFunctionDefinition 22 | import com.jetbrains.cidr.lang.psi.OCLambdaExpression 23 | import com.jetbrains.cidr.lang.psi.OCMethod 24 | import icons.CPPCoverageIcons 25 | import net.zero9178.cov.util.toCP 26 | import java.awt.Point 27 | import java.awt.event.ComponentAdapter 28 | import java.awt.event.ComponentEvent 29 | import java.awt.event.HierarchyBoundsAdapter 30 | import java.awt.event.HierarchyEvent 31 | import kotlin.math.min 32 | 33 | private const val MAX_COMBO_BOX_WIDTH = 200 34 | 35 | val DUPLICATE_FUNCTION_GROUP = Key>("DUPLICATE_FUNCTION_GROUP") 36 | 37 | class CoverageTemplateLineMarkerProvider : LineMarkerProvider { 38 | 39 | override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { 40 | if (element !is LeafPsiElement || (element.elementType !== OCTokenTypes.LBRACE && element.elementType !== OCTokenTypes.IDENTIFIER)) { 41 | return null 42 | } 43 | val project = element.project 44 | val highlighter = CoverageHighlighter.getInstance(project) 45 | val psiFile = element.containingFile ?: return null 46 | val document = PsiDocumentManager.getInstance(project).getDocument(psiFile) ?: return null 47 | val file = psiFile.virtualFile ?: return null 48 | val info = highlighter.highlighting[file] ?: return null 49 | val line = document.getLineNumber(element.startOffset) 50 | val column = element.startOffset - document.getLineStartOffset(line) 51 | var region = (line + 1) toCP (column + 1) 52 | var group = info[region] 53 | if (group == null) { 54 | // GCC does not carry any information about the column, just the line. Retry by searching for the line and column 0 55 | region = (line + 1) toCP 0 56 | group = info[region] ?: return null 57 | // If there are multiple lambdas, function definitions or whatever on a line we can't differentiate them 58 | // as GCC only gives line info so we need to make sure ourselves that we don't create multiple line 59 | // markers for the same group 60 | val duplicates = project.getUserData(DUPLICATE_FUNCTION_GROUP) 61 | if (duplicates != null) { 62 | if (duplicates.contains(group)) { 63 | return null 64 | } 65 | duplicates.add(group) 66 | } else { 67 | project.putUserData(DUPLICATE_FUNCTION_GROUP, mutableSetOf(group)) 68 | } 69 | } 70 | if (group.functions.size <= 1) { 71 | return null 72 | } 73 | val callable = element.parents(true).find { 74 | when (it) { 75 | is OCFunctionDefinition -> true 76 | is OCMethod -> true 77 | is OCLambdaExpression -> true 78 | else -> false 79 | } 80 | } 81 | val attach = when (callable) { 82 | is OCFunctionDefinition -> callable.nameIdentifier 83 | is OCMethod -> callable.nameIdentifier 84 | is OCLambdaExpression -> callable.lambdaIntroducer.firstChild 85 | else -> null 86 | } ?: return null 87 | 88 | return object : LineMarkerInfo(attach, attach.textRange, CPPCoverageIcons.TEMPLATE_LINE_MARKER, { 89 | "Change displayed template instantiation coverage" 90 | }, null, GutterIconRenderer.Alignment.RIGHT, { 91 | "Change displayed template instantiation coverage" 92 | }) { 93 | override fun createGutterRenderer(): GutterIconRenderer { 94 | return object : LineMarkerGutterIconRenderer(this) { 95 | override fun getClickAction(): AnAction = 96 | TemplateInstantiationsChoosePopup(group, highlighter, this) 97 | 98 | override fun isNavigateAction(): Boolean { 99 | return true 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | private class TemplateInstantiationsChoosePopup( 107 | val group: CoverageHighlighter.HighlightFunctionGroup, 108 | val highlighter: CoverageHighlighter, 109 | val renderer: GutterIconRenderer 110 | ) : AnAction() { 111 | override fun actionPerformed(e: AnActionEvent) { 112 | val editor = e.getData(CommonDataKeys.EDITOR) as? EditorEx ?: return 113 | val gutterComponent = editor.gutterComponentEx 114 | var point = gutterComponent.getCenterPoint(renderer) 115 | if (point == null) { // disabled gutter icons for example 116 | point = Point( 117 | gutterComponent.width, 118 | editor.visualPositionToXY(editor.caretModel.visualPosition).y + editor.lineHeight / 2 119 | ) 120 | } 121 | val comboBox = ComboBox(group.functions.keys.toTypedArray()) 122 | val maxLength = group.functions.keys.maxOf { it.length } 123 | comboBox.prototypeDisplayValue = "".padStart(min(maxLength, MAX_COMBO_BOX_WIDTH), '0') 124 | comboBox.addActionListener { 125 | if (Disposer.isDisposed(group)) { 126 | return@addActionListener 127 | } 128 | highlighter.changeActive(group, comboBox.item) 129 | } 130 | val balloon = JBPopupFactory.getInstance().createDialogBalloonBuilder( 131 | comboBox, 132 | null 133 | ).apply { 134 | setHideOnClickOutside(true) 135 | setCloseButtonEnabled(false) 136 | setAnimationCycle(0) 137 | setBlockClicksThroughBalloon(true) 138 | }.createBalloon() 139 | val moveListener = object : ComponentAdapter() { 140 | override fun componentMoved(e: ComponentEvent?) { 141 | balloon.hide() 142 | } 143 | } 144 | gutterComponent.addComponentListener(moveListener) 145 | Disposer.register(balloon) { 146 | gutterComponent.removeComponentListener(moveListener) 147 | } 148 | val hierarchyListener = object : HierarchyBoundsAdapter() { 149 | override fun ancestorMoved(e: HierarchyEvent?) { 150 | balloon.hide() 151 | } 152 | } 153 | gutterComponent.addHierarchyBoundsListener(hierarchyListener) 154 | Disposer.register(balloon) { 155 | gutterComponent.removeHierarchyBoundsListener(hierarchyListener) 156 | } 157 | balloon.show(RelativePoint(gutterComponent, point), Balloon.Position.above) 158 | } 159 | } 160 | 161 | 162 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C/C++ Coverage for CLion 2 | 3 | ![alt text][logo] 4 | 5 | [logo]:https://i.imgur.com/PNvQs9j.png "View in the Editor" 6 | 7 | ## Content 8 | 1. [Getting started](#getting-started) 9 | 1. [Tools](#tools) 10 | 2. [Compiling with Coverage](#compiling-with-coverage) 11 | 3. [Running](#running) 12 | 2. [Differences in compilers](#differences-in-compilers) 13 | 3. [Known Issues/TODOs](#known-issuestodos) 14 | 15 | ## Getting Started 16 | 17 | ### Tools 18 | 19 | To get coverage you need one of the following compilers: Clang(-cl) 5 or later, GCC 6 or later. 20 | Recommended are Clang 6 onwards and GCC 9 onwards. GCC 6 to 8 are not recommended for larger projects and cannot gather 21 | branch coverage. To see why refer to [Differences in compilers](#differences-in-compilers). Both of them use very different ways to gather coverage. 22 | GCC ships a tool called gcov while clang requires llvm-cov and llvm-profdata. Make sure the tools versions matches your 23 | compilers version. When the plugin sees an empty path for either of the tools it will attempt to guess the correct paths 24 | based on C and C++ compiler paths and prefixes and suffixes in their name. Optionally One can specify a demangler when 25 | using Clang to demangle C++ symbols. This is not required with GCC as gcov has a built in demangler. Tested and 26 | supported demanglers are c++filt and llvm-cxxfilt or llvm-undname when using clang-cl. 27 | You can specify your gcov and llvm-cov per toolchain by going into the settings menu under `Language & Frameworks -> C/C++ Coverage`. 28 | There you can also configure different settings related to processing coverage 29 | 30 | ### Compiling with Coverage 31 | 32 | To compile with coverage each of the two compilers require different flags. In the case of GCC we need the `--coverage` flag 33 | for the compiler. On some platforms one needs to explicitly link against `gcov`. For clang we need `-fprofile-instr-generate -fcoverage-mapping` 34 | for compiler flags and `-fprofile-instr-generate` for linker flags. On some platform one may also need to explicitly link 35 | again the libclang_rt.profile-\. An example cmake sections is shown here: 36 | ```cmake 37 | if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") 38 | add_compile_options(-fprofile-instr-generate -fcoverage-mapping) 39 | add_link_options(-fprofile-instr-generate) 40 | #Uncomment in case of linker errors 41 | #link_libraries(clang_rt.profile-x86_64) 42 | elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 43 | add_compile_options(--coverage) 44 | #Uncomment in case of linker errors 45 | #link_libraries(gcov) 46 | endif () 47 | ``` 48 | 49 | Alternatively one can enable in the settings to not use auto flag detection. Instead a 'Run Coverage' button will appear 50 | with which one can explicitly choose to gather coverage data 51 | 52 | If your installation of clang does not have the needed profile library you will need to compile it yourself 53 | from https://github.com/llvm/llvm-project/tree/master/compiler-rt. Make sure it matches your compilers version 54 | 55 | #### Note: 56 | When using Clang 8 or older on Windows with the GNU target you'll most likely get a linker error due to the symbol `lprofGetHostName` missing. 57 | This is due to a bug inside llvm's compiler-rt. To circumvent this add the following lines of code to one of your files: 58 | ```cpp 59 | #ifdef __clang__ 60 | #if _WIN32 && __clang_major__ == 8 && __clang_minor__ == 0 && \ 61 | __clang_patchlevel__ == 0 62 | 63 | #include 64 | 65 | extern "C" int lprofGetHostName(char *Name, int Len) { 66 | WCHAR Buffer[128]; 67 | DWORD BufferSize = sizeof(Buffer); 68 | BOOL Result = 69 | GetComputerNameExW(ComputerNameDnsFullyQualified, Buffer, &BufferSize); 70 | if (!Result) 71 | return -1; 72 | if (WideCharToMultiByte(CP_UTF8, 0, Buffer, -1, Name, Len, nullptr, 73 | nullptr) == 0) 74 | return -1; 75 | return 0; 76 | } 77 | 78 | #endif 79 | #endif 80 | 81 | ``` 82 | I recommend putting it into the main file of your testing framework eg. 83 | 84 | ### Running 85 | 86 | Similar to how sanitizers work in CLion, coverage is automatically gathered when the right compiler flags are detected. 87 | After your executable has terminated a modal dialog will open telling you that coverage is being 88 | gathered and prohibiting you from editing the source code. As soon as it finishes the tool window 89 | on the right will open and display your statistics. You can double click either on a file or a 90 | function to navigate to said symbol or you can click on the header above to sort by a metric. By default 91 | only source files inside of your project will be displayed. By checking the checkbox on the top right every file compiled 92 | into your project will be shown. Using the clear button will clear all editors and the tool window. 93 | 94 | ## Differences in compilers 95 | 96 | This plugin implements 3 different coverage gathers. Clang 5 onwards, GCC 9 onwards and GCC 6 to 8. 97 | GCC 6 to 8 is not recommended and cannot produce branch coverage like the other two. 98 | The most powerful of these implementations is Clang. 99 | 100 | Clang doesn't use line coverage but instead uses region coverage and has no branch coverage on its own. A region is a 101 | block of code that has the same execution count and is specified from start to end 102 | character. This precise data allows this plugin to generate branch coverage for Clang via postprocessing by making a query 103 | into the CLion parser structure and comparing the execution counts of neighbouring regions. Region coverage also 104 | allows checking if operands of boolean operators that short circuit have been evaluated or not. 105 | 106 | GCC 9 and onwards on the other hand produces line and branch coverage. The problem with this approach is 107 | that the data is less precise and has major problems when there are multiple statements on a single line. 108 | Branch coverage is also given per line and the plugin needs to make a query into the line to match up the branches specified 109 | by gcov to the statements in the source code. Another issue is that GCC generates a branch for every expression that may 110 | throw an exception. As 99% of all expressions that could throw an exception never do and showing such branches would generate 111 | incredible amount of noise in the editor the plugin filters them out. 112 | 113 | Previous versions of gcc generated very similar information as GCC 9. Branch coverage however does not carry information 114 | as to where a branch comes from and as all newer versions of gcc implement the GCC 9 format or newer, I decided not 115 | to try and implement branch coverage for GCC 6 to 8. Line coverage however should work as intended. Please note that 116 | gcov versions 6 to 8 crash easily on large projects. 117 | 118 | ## Known Issues/TODOs 119 | 120 | * Due to the plugin needing the parser structure of source files coverage gathering is paused and later resumed if 121 | indexing is in progress 122 | * This plugin is untested on Mac OS-X. It should work as long as the toolchains are setup correctly. One thing to watch 123 | out for is that gcc, g++ and gcov installed by XCode are **NOT** GCC but actually clang,clang++ and a gcov like implementation 124 | by the LLVM project. This version of gcov will **NOT WORK** as it does not implement the same command line arguments as 125 | real gcov. Please install another toolchain or find a llvm-cov version suitable for your version of clang. 126 | * Currently a new function is generated for each type in a template instantiations. if constexpr 127 | also has weird data and in general data in such functions may be less accurate. Future versions will 128 | try to address this issue and possibly even parse the function name in order to collapse different instantiations of the 129 | same function 130 | * Remote Machine is currently in WIP and requires some restructuring of the Architecture. WSL is supported 131 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 68 | 75 | 82 | 89 | 96 | 103 | 110 | 117 | 120 | 125 | 130 | 135 | 137 | 142 | 143 | 145 | 149 | 150 | 152 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 68 | 75 | 82 | 89 | 96 | 103 | 110 | 117 | 120 | 125 | 130 | 135 | 137 | 142 | 143 | 145 | 149 | 150 | 152 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/main/resources/icons/TemplateLineMarker.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 37 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 54 | 62 | 70 | 78 | 86 | 94 | 102 | 110 | 118 | 126 | 129 | 136 | 138 | 145 | 152 | 159 | 166 | 173 | 180 | 187 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /src/main/java/net/zero9178/cov/window/SettingsWindow.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 |
180 | -------------------------------------------------------------------------------- /src/main/resources/icons/TemplateLineMarker_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 58 | 66 | 74 | 82 | 90 | 98 | 106 | 114 | 122 | 130 | 138 | 146 | 154 | 162 | 170 | 178 | 186 | 194 | 202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/window/SettingsWindowImpl.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.window 2 | 3 | import com.intellij.icons.AllIcons 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.fileChooser.FileChooserDescriptor 6 | import com.intellij.openapi.ui.TextBrowseFolderListener 7 | import com.intellij.openapi.ui.TextFieldWithBrowseButton 8 | import com.intellij.openapi.vfs.VirtualFile 9 | import com.intellij.util.io.exists 10 | import com.jetbrains.cidr.cpp.toolchains.CPPToolchains 11 | import com.jetbrains.cidr.cpp.toolchains.CPPToolchainsListener 12 | import com.jetbrains.cidr.cpp.toolchains.WSL 13 | import net.zero9178.cov.settings.CoverageGeneratorSettings 14 | import java.awt.event.ItemEvent 15 | import java.awt.event.KeyEvent 16 | import java.awt.event.KeyListener 17 | import java.nio.file.Paths 18 | import javax.swing.DefaultComboBoxModel 19 | 20 | class SettingsWindowImpl : SettingsWindow() { 21 | 22 | init { 23 | myLLVMProfdataBrowser.isVisible = false 24 | myLLVMProfLabel.isVisible = false 25 | myDemanglerBrowser.isVisible = false 26 | myDemanglerLabel.isVisible = false 27 | 28 | ApplicationManager.getApplication().messageBus.connect() 29 | .subscribe(CPPToolchainsListener.TOPIC, object : CPPToolchainsListener { 30 | override fun toolchainsRenamed(renamed: MutableMap) { 31 | for (renames in renamed) { 32 | val value = myTempToolchainState[renames.key] ?: continue 33 | myTempToolchainState.remove(renames.key) 34 | myTempToolchainState[renames.value] = value 35 | } 36 | updateToolChainComboBox() 37 | updateUIAfterItemChange() 38 | } 39 | 40 | override fun toolchainCMakeEnvironmentChanged(toolchains: MutableSet) { 41 | updateToolChainComboBox() 42 | toolchains.groupBy { 43 | CPPToolchains.getInstance().toolchains.contains(it) 44 | }.forEach { group -> 45 | if (group.key) { 46 | group.value.forEach { 47 | //I am not sure at all yet if one can assume the order of the notification delivery. For now lets 48 | //just be happy if the order was correct (CoverageGeneratorPaths.kt was called first) and if not 49 | //do an empty string 50 | myTempToolchainState[it.name] = 51 | CoverageGeneratorSettings.getInstance().paths.getOrDefault( 52 | it.name, 53 | CoverageGeneratorSettings.GeneratorInfo() 54 | ) 55 | } 56 | } else { 57 | group.value.forEach { 58 | myTempToolchainState.remove(it.name) 59 | } 60 | } 61 | } 62 | } 63 | }) 64 | updateToolChainComboBox() 65 | 66 | class MyTextBrowseFolderListener( 67 | textFieldWithBrowseButton: TextFieldWithBrowseButton, 68 | val runnable: (String, CoverageGeneratorSettings.GeneratorInfo, String) -> Unit 69 | ) : 70 | TextBrowseFolderListener( 71 | FileChooserDescriptor(true, false, false, false, false, false) 72 | ), KeyListener { 73 | 74 | init { 75 | textFieldWithBrowseButton.addBrowseFolderListener(this) 76 | textFieldWithBrowseButton.textField.addKeyListener(this) 77 | } 78 | 79 | override fun onFileChosen(chosenFile: VirtualFile) { 80 | super.onFileChosen(chosenFile) 81 | textChanged() 82 | } 83 | 84 | private fun textChanged() { 85 | val selectedItem = myComboBox.selectedItem as? String ?: return 86 | val info = myTempToolchainState[selectedItem] ?: return 87 | runnable(componentText, info, selectedItem) 88 | } 89 | 90 | override fun keyReleased(e: KeyEvent?) { 91 | textChanged() 92 | } 93 | 94 | override fun keyTyped(e: KeyEvent?) {} 95 | 96 | override fun keyPressed(e: KeyEvent?) {} 97 | } 98 | 99 | MyTextBrowseFolderListener(myGcovOrllvmCovBrowser) { text, info, selectedItem -> 100 | info.gcovOrllvmCovPath = text 101 | updateLLVMFields(CPPToolchains.getInstance().toolchains.find { it.name == selectedItem }?.wsl) 102 | } 103 | 104 | MyTextBrowseFolderListener(myLLVMProfdataBrowser) { text, info, _ -> 105 | info.llvmProfDataPath = text 106 | } 107 | 108 | MyTextBrowseFolderListener(myDemanglerBrowser) { text, info, _ -> 109 | info.demangler = text 110 | } 111 | 112 | myComboBox.addItemListener { 113 | if (it.stateChange == ItemEvent.SELECTED) { 114 | updateUIAfterItemChange() 115 | } 116 | } 117 | 118 | myDocHyperlink.setHyperlinkTarget("https://github.com/zero9178/C-Cpp-Coverage-for-CLion") 119 | myDocHyperlink.setTextWithHyperlink("For more information see: https://github.com/zero9178/C-Cpp-Coverage-for-CLion") 120 | myIfBranchCoverage.isSelected = CoverageGeneratorSettings.getInstance().ifBranchCoverageEnabled 121 | myLoopBranchCoverage.isSelected = CoverageGeneratorSettings.getInstance().loopBranchCoverageEnabled 122 | myCondBranchCoverage.isSelected = CoverageGeneratorSettings.getInstance().conditionalExpCoverageEnabled 123 | myBooleanOpBranchCoverage.isSelected = CoverageGeneratorSettings.getInstance().booleanOpBranchCoverageEnabled 124 | myDoBranchCoverage.isSelected = CoverageGeneratorSettings.getInstance().branchCoverageEnabled 125 | myCalculateExternal.isSelected = CoverageGeneratorSettings.getInstance().calculateExternalSources 126 | } 127 | 128 | private fun updateToolChainComboBox() { 129 | myComboBox.model = DefaultComboBoxModel(CPPToolchains.getInstance().toolchains.map { it.name }.toTypedArray()) 130 | } 131 | 132 | private fun updateUIAfterItemChange() { 133 | val toolchainName = myComboBox.selectedItem as? String ?: return 134 | val wsl = CPPToolchains.getInstance().toolchains.find { 135 | it.name == toolchainName 136 | }?.wsl 137 | myGcovOrllvmCovBrowser.text = myTempToolchainState[toolchainName]?.gcovOrllvmCovPath ?: "" 138 | myLLVMProfdataBrowser.text = myTempToolchainState[toolchainName]?.llvmProfDataPath ?: "" 139 | myDemanglerBrowser.text = myTempToolchainState[toolchainName]?.demangler ?: "" 140 | updateLLVMFields(wsl) 141 | } 142 | 143 | private fun updateLLVMFields(wsl: WSL?) { 144 | if (myGcovOrllvmCovBrowser.text.isBlank()) { 145 | myErrors.text = "No executable specified" 146 | myErrors.icon = AllIcons.General.Warning 147 | myLLVMProfLabel.isVisible = false 148 | myLLVMProfdataBrowser.isVisible = false 149 | myDemanglerLabel.isVisible = false 150 | myDemanglerBrowser.isVisible = false 151 | myGcovOrLLVMCovLabel.text = "gcov or llvm-cov:" 152 | return 153 | } 154 | if (if (wsl == null) !Paths.get(myGcovOrllvmCovBrowser.text).exists() else false) { 155 | myErrors.text = "'${myGcovOrllvmCovBrowser.text}' is not a valid path to an executable" 156 | myErrors.icon = AllIcons.General.Warning 157 | myLLVMProfLabel.isVisible = false 158 | myLLVMProfdataBrowser.isVisible = false 159 | myDemanglerLabel.isVisible = false 160 | myDemanglerBrowser.isVisible = false 161 | myGcovOrLLVMCovLabel.text = "gcov or llvm-cov:" 162 | return 163 | } 164 | if (!myGcovOrllvmCovBrowser.text.contains("(llvm-cov|gcov)".toRegex(RegexOption.IGNORE_CASE))) { 165 | myErrors.text = "'${myGcovOrllvmCovBrowser.text}' is neither gcov nor llvm-cov" 166 | myErrors.icon = AllIcons.General.Warning 167 | myLLVMProfLabel.isVisible = false 168 | myLLVMProfdataBrowser.isVisible = false 169 | myDemanglerLabel.isVisible = false 170 | myDemanglerBrowser.isVisible = false 171 | myGcovOrLLVMCovLabel.text = "gcov or llvm-cov:" 172 | return 173 | } 174 | myErrors.text = "" 175 | myErrors.icon = null 176 | myLLVMProfLabel.isVisible = myGcovOrllvmCovBrowser.text.contains("llvm-cov", true) 177 | myLLVMProfdataBrowser.isVisible = myLLVMProfLabel.isVisible 178 | myDemanglerLabel.isVisible = myLLVMProfLabel.isVisible 179 | myDemanglerBrowser.isVisible = myLLVMProfLabel.isVisible 180 | myGcovOrLLVMCovLabel.text = if (myLLVMProfLabel.isVisible) "llvm-cov:" else "gcov:" 181 | } 182 | 183 | private val myTempToolchainState: MutableMap = 184 | CoverageGeneratorSettings.getInstance().paths.mapValues { it.value.copy() }.toMutableMap() 185 | 186 | //toMutableMap creates a copy of the map instead of copying the reference 187 | 188 | init { 189 | updateUIAfterItemChange() 190 | } 191 | 192 | override fun isModified() = 193 | CoverageGeneratorSettings.getInstance().paths != myTempToolchainState || myIfBranchCoverage.isSelected != CoverageGeneratorSettings.getInstance().ifBranchCoverageEnabled 194 | || myLoopBranchCoverage.isSelected != CoverageGeneratorSettings.getInstance().loopBranchCoverageEnabled 195 | || myCondBranchCoverage.isSelected != CoverageGeneratorSettings.getInstance().conditionalExpCoverageEnabled 196 | || myBooleanOpBranchCoverage.isSelected != CoverageGeneratorSettings.getInstance().booleanOpBranchCoverageEnabled 197 | || myDoBranchCoverage.isSelected != CoverageGeneratorSettings.getInstance().branchCoverageEnabled 198 | || myCalculateExternal.isSelected != CoverageGeneratorSettings.getInstance().calculateExternalSources 199 | 200 | override fun getDisplayName() = "C/C++ Coverage" 201 | 202 | override fun apply() { 203 | CoverageGeneratorSettings.getInstance().paths = 204 | myTempToolchainState.mapValues { it.value.copy() }.toMutableMap() 205 | CoverageGeneratorSettings.getInstance().ifBranchCoverageEnabled = myIfBranchCoverage.isSelected 206 | CoverageGeneratorSettings.getInstance().loopBranchCoverageEnabled = myLoopBranchCoverage.isSelected 207 | CoverageGeneratorSettings.getInstance().conditionalExpCoverageEnabled = myCondBranchCoverage.isSelected 208 | CoverageGeneratorSettings.getInstance().booleanOpBranchCoverageEnabled = myBooleanOpBranchCoverage.isSelected 209 | CoverageGeneratorSettings.getInstance().branchCoverageEnabled = myDoBranchCoverage.isSelected 210 | CoverageGeneratorSettings.getInstance().calculateExternalSources = myCalculateExternal.isSelected 211 | } 212 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/data/GCCGCDACoverageGenerator.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.data 2 | 3 | import com.github.h0tk3y.betterParse.combinators.* 4 | import com.github.h0tk3y.betterParse.grammar.Grammar 5 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 6 | import com.github.h0tk3y.betterParse.lexer.literalToken 7 | import com.github.h0tk3y.betterParse.lexer.regexToken 8 | import com.github.h0tk3y.betterParse.parser.ParseException 9 | import com.intellij.execution.configurations.GeneralCommandLine 10 | import com.intellij.notification.NotificationGroupManager 11 | import com.intellij.notification.NotificationType 12 | import com.intellij.openapi.progress.ProgressIndicator 13 | import com.intellij.openapi.progress.ProgressManager 14 | import com.intellij.openapi.project.Project 15 | import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace 16 | import com.jetbrains.cidr.cpp.execution.CMakeAppRunConfiguration 17 | import com.jetbrains.cidr.cpp.execution.CMakeBuildProfileExecutionTarget 18 | import com.jetbrains.cidr.cpp.toolchains.CPPEnvironment 19 | import com.jetbrains.cidr.execution.ConfigurationExtensionContext 20 | import net.zero9178.cov.settings.CoverageGeneratorSettings 21 | import net.zero9178.cov.util.toCP 22 | import java.nio.file.Files 23 | import java.nio.file.Paths 24 | import java.util.concurrent.CompletableFuture 25 | import java.util.concurrent.CompletionException 26 | import kotlin.math.ceil 27 | 28 | class GCCGCDACoverageGenerator(private val myGcov: String, private val myMajorVersion: Int) : 29 | CoverageGenerator { 30 | 31 | private sealed class Item { 32 | class File(val path: String) : Item() 33 | 34 | class Function(val startLine: Int, val endLine: Int, val count: Long, val name: String) : Item() 35 | 36 | class LCount(val line: Int, val count: Long) : Item() 37 | 38 | class Branch(val line: Int, val branchType: BranchType) : Item() { 39 | enum class BranchType { 40 | notexec, 41 | taken, 42 | nottaken 43 | } 44 | } 45 | } 46 | 47 | private fun parseGcovIR( 48 | lines: List>, 49 | project: Project, 50 | env: CPPEnvironment 51 | ): CoverageData { 52 | 53 | abstract class GCovCommonGrammar : Grammar>() { 54 | 55 | //Lexems 56 | val num by regexToken("\\d+") 57 | val comma by literalToken(",") 58 | val colon by literalToken(":") 59 | val newLine by literalToken("\n") 60 | val ws by regexToken("[ \t]+", ignore = true) 61 | val file by regexToken("file:.*") 62 | val function by regexToken("function:.*") 63 | val version by regexToken("version:.*\n", ignore = true) 64 | 65 | //Keywords 66 | val lcount by literalToken("lcount") 67 | val branch by literalToken("branch") 68 | val notexec by literalToken("notexec") 69 | val taken by literalToken("taken") 70 | val nottaken by literalToken("nottaken") 71 | val nonKeyword by regexToken("[a-zA-Z_]\\w*") 72 | 73 | val word by nonKeyword or file or function or lcount or branch or notexec or nottaken 74 | 75 | val fileLine by file use { 76 | Item.File(env.toLocalPath(text.removePrefix("file:")).replace('\\', '/')) 77 | } 78 | 79 | val branchLine by -branch and -colon and num and -comma and (notexec or taken or nottaken) map { (count, type) -> 80 | Item.Branch(count.text.toInt(), Item.Branch.BranchType.valueOf(type.text)) 81 | } 82 | } 83 | 84 | val govUnder8Grammer = object : GCovCommonGrammar() { 85 | 86 | val functionLine by function use { 87 | object : Grammar() { 88 | 89 | val num by regexToken("\\d+") 90 | val comma by literalToken(",") 91 | val rest by regexToken(".*") 92 | 93 | override val rootParser by num and -comma and num and -comma and rest map { (line, count, name) -> 94 | Item.Function(line.text.toInt(), -1, count.text.toLong(), name.text) 95 | } 96 | }.parseToEnd(text.removePrefix("function:")) 97 | } 98 | 99 | val lcountLine by -lcount and -colon and num and -comma and num map { (line, count) -> 100 | Item.LCount(line.text.toInt(), count.text.toLong()) 101 | } 102 | 103 | override val rootParser by separatedTerms( 104 | fileLine or functionLine or lcountLine or branchLine, 105 | newLine 106 | ) 107 | } 108 | 109 | val gcov8Grammer = object : GCovCommonGrammar() { 110 | 111 | val functionLine by function use { 112 | object : Grammar() { 113 | 114 | val num by regexToken("\\d+") 115 | val comma by literalToken(",") 116 | val rest by regexToken(".*") 117 | 118 | override val rootParser by num and -comma and num and -comma and num and -comma and rest map { (startLine, endLine, count, name) -> 119 | Item.Function(startLine.text.toInt(), endLine.text.toInt(), count.text.toLong(), name.text) 120 | } 121 | }.parseToEnd(text.removePrefix("function:")) 122 | } 123 | 124 | val lcountLine by -lcount and -colon and num and -comma and num and -comma and -num map { (line, count) -> 125 | Item.LCount(line.text.toInt(), count.text.toLong()) 126 | } 127 | 128 | override val rootParser by separatedTerms( 129 | fileLine or functionLine or lcountLine or branchLine, 130 | newLine 131 | ) 132 | } 133 | 134 | val result = lines.chunked(ceil(lines.size.toDouble() / Thread.activeCount()).toInt()).map { 135 | try { 136 | CompletableFuture.supplyAsync { 137 | it.flatMap { gcovFile -> 138 | ProgressManager.checkCanceled() 139 | try { 140 | val ast = if (myMajorVersion == 8) { 141 | gcov8Grammer.parseToEnd(gcovFile.joinToString("\n")) 142 | } else { 143 | govUnder8Grammer.parseToEnd(gcovFile.joinToString("\n")) 144 | } 145 | ast.filter { it !is Item.Branch } 146 | } catch (e: ParseException) { 147 | NotificationGroupManager.getInstance().getNotificationGroup("C/C++ Coverage Notification") 148 | .createNotification( 149 | "Error parsing gcov generated files", 150 | "Parser output:${e.errorResult}", 151 | NotificationType.ERROR 152 | ).setSubtitle("This is either due to a bug in the plugin or gcov").notify(project) 153 | emptyList() 154 | } 155 | } 156 | } 157 | } catch (e: CompletionException) { 158 | val cause = e.cause 159 | if (cause != null) { 160 | throw cause 161 | } else { 162 | throw e 163 | } 164 | } 165 | }.flatMap { it.get() } 166 | return linesToCoverageData(result) 167 | } 168 | 169 | private fun linesToCoverageData(lines: List): CoverageData { 170 | val files = mutableListOf() 171 | var lineCopy = lines 172 | while (lineCopy.isNotEmpty()) { 173 | val item = lineCopy[0] 174 | lineCopy = lineCopy.subList(1, lineCopy.size) 175 | val file = item as? Item.File ?: continue 176 | val functions = mutableListOf>>() 177 | lineCopy = lineCopy.dropWhile { 178 | if (it is Item.Function) { 179 | functions += Triple(it.startLine, it.name, mutableMapOf()) 180 | true 181 | } else { 182 | false 183 | } 184 | } 185 | lineCopy = lineCopy.dropWhile { 186 | if (it is Item.LCount) { 187 | val func = functions.findLast { function -> function.first <= it.line } ?: return@dropWhile true 188 | func.third[it.line] = it.count 189 | true 190 | } else { 191 | false 192 | } 193 | } 194 | if (functions.isEmpty()) { 195 | continue 196 | } 197 | files += CoverageFileData(file.path, functions.map { (startLine, name, lines) -> 198 | CoverageFunctionData( 199 | startLine toCP 0, Int.MAX_VALUE toCP 0, name, FunctionLineData(lines), 200 | emptyList() 201 | ) 202 | }.associateBy { it.functionName }) 203 | } 204 | 205 | return CoverageData( 206 | files.associateBy { it.filePath }, 207 | false, 208 | CoverageGeneratorSettings.getInstance().calculateExternalSources 209 | ) 210 | } 211 | 212 | override fun generateCoverage( 213 | configuration: CMakeAppRunConfiguration, 214 | environment: CPPEnvironment, 215 | executionTarget: CMakeBuildProfileExecutionTarget, 216 | indicator: ProgressIndicator, 217 | context: ConfigurationExtensionContext 218 | ): CoverageData? { 219 | val config = CMakeWorkspace.getInstance(configuration.project).getCMakeConfigurationFor( 220 | configuration.getResolveConfiguration(executionTarget) 221 | ) ?: return null 222 | val files = 223 | config.configurationGenerationDir.walkTopDown().filter { 224 | it.isFile && it.name.endsWith(".gcda") 225 | }.map { environment.toEnvPath(it.absolutePath) }.toList() 226 | 227 | val p = environment.hostMachine.runProcess( 228 | GeneralCommandLine( 229 | listOf( 230 | myGcov, 231 | "-i", 232 | "-m" 233 | ) + files 234 | ).withWorkDirectory(config.configurationGenerationDir), indicator, -1 235 | ) 236 | indicator.checkCanceled() 237 | val lines = p.stdout 238 | val retCode = p.exitCode 239 | if (retCode != 0) { 240 | NotificationGroupManager.getInstance().getNotificationGroup("C/C++ Coverage Notification") 241 | .createNotification( 242 | "gcov returned error code $retCode", 243 | "Invocation: ${ 244 | (listOf( 245 | myGcov, 246 | "-i", 247 | "-m" 248 | ) + files).joinToString(" ") 249 | }\n Stderr: $lines", 250 | NotificationType.ERROR 251 | ).setSubtitle("Invocation and error output:").notify(configuration.project) 252 | return null 253 | } 254 | 255 | files.forEach { Files.deleteIfExists(Paths.get(environment.toLocalPath(it))) } 256 | 257 | val filter = config.configurationGenerationDir.listFiles()?.filter { 258 | it.isFile && it.name.endsWith(".gcov") 259 | }?.toList() ?: emptyList() 260 | 261 | val output = filter.map { 262 | it.readLines() 263 | } 264 | 265 | filter.forEach { it.delete() } 266 | 267 | return parseGcovIR(output, configuration.project, environment) 268 | } 269 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/editor/CoverageHighlighter.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.editor 2 | 3 | import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer 4 | import com.intellij.icons.AllIcons 5 | import com.intellij.openapi.Disposable 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.components.service 8 | import com.intellij.openapi.editor.* 9 | import com.intellij.openapi.editor.colors.CodeInsightColors 10 | import com.intellij.openapi.editor.colors.EditorColorsListener 11 | import com.intellij.openapi.editor.colors.EditorColorsManager 12 | import com.intellij.openapi.editor.colors.EditorColorsUtil 13 | import com.intellij.openapi.editor.event.EditorFactoryEvent 14 | import com.intellij.openapi.editor.event.EditorFactoryListener 15 | import com.intellij.openapi.editor.ex.MarkupModelEx 16 | import com.intellij.openapi.editor.markup.* 17 | import com.intellij.openapi.fileEditor.FileDocumentManager 18 | import com.intellij.openapi.project.Project 19 | import com.intellij.openapi.util.Disposer 20 | import com.intellij.openapi.vfs.LocalFileSystem 21 | import com.intellij.openapi.vfs.VirtualFile 22 | import com.intellij.util.IconUtil 23 | import com.jetbrains.rd.util.first 24 | import net.zero9178.cov.data.CoverageData 25 | import net.zero9178.cov.data.FunctionLineData 26 | import net.zero9178.cov.data.FunctionRegionData 27 | import net.zero9178.cov.util.ComparablePair 28 | import net.zero9178.cov.util.toCP 29 | import java.awt.* 30 | import java.nio.file.InvalidPathException 31 | import java.nio.file.Paths 32 | 33 | class CoverageHighlighter(private val myProject: Project) : Disposable { 34 | 35 | companion object { 36 | fun getInstance(project: Project) = project.service() 37 | } 38 | 39 | init { 40 | EditorFactory.getInstance().addEditorFactoryListener(object : EditorFactoryListener { 41 | override fun editorCreated(event: EditorFactoryEvent) { 42 | val editor = event.editor 43 | if (editor.project !== myProject) { 44 | return 45 | } 46 | applyOnEditor(editor) 47 | } 48 | 49 | override fun editorReleased(event: EditorFactoryEvent) { 50 | val editor = event.editor 51 | if (editor.project !== myProject) { 52 | return 53 | } 54 | removeFromEditor(editor) 55 | } 56 | }, this) 57 | ApplicationManager.getApplication().messageBus.connect(this) 58 | .subscribe(EditorColorsManager.TOPIC, EditorColorsListener { 59 | clear() 60 | EditorFactory.getInstance().allEditors.forEach(::applyOnEditor) 61 | }) 62 | } 63 | 64 | private class MyEditorCustomElementRenderer(val editor: Editor, val fullCoverage: Boolean) : 65 | EditorCustomElementRenderer { 66 | override fun paint( 67 | inlay: Inlay<*>, 68 | g: Graphics, 69 | targetRegion: Rectangle, 70 | textAttributes: TextAttributes 71 | ) { 72 | val margin = 1 73 | val icon = IconUtil.toSize( 74 | if (fullCoverage) AllIcons.Actions.Commit else AllIcons.General.Error, 75 | targetRegion.height - 2 * margin, 76 | targetRegion.height - 2 * margin 77 | ) 78 | 79 | val graphics = g.create() as Graphics2D 80 | graphics.composite = AlphaComposite.SrcAtop.derive(1.0f) 81 | icon.paintIcon(editor.component, graphics, targetRegion.x + margin, targetRegion.y + margin) 82 | graphics.dispose() 83 | } 84 | 85 | override fun calcWidthInPixels(inlay: Inlay<*>): Int { 86 | return calcHeightInPixels(inlay) 87 | } 88 | } 89 | 90 | private fun applyOnHighlightFunction(editor: Editor, functionGroup: HighlightFunctionGroup) { 91 | val rangesInFunction = myActiveHighlighting.getOrPut(editor) { mutableMapOf() } 92 | val ranges = rangesInFunction.getOrPut(functionGroup) { mutableListOf() } 93 | val value = functionGroup.functions[functionGroup.active] ?: return 94 | val colorScheme = EditorColorsUtil.getGlobalOrDefaultColorScheme() 95 | val markupModel = editor.markupModel as? MarkupModelEx ?: return 96 | for ((start, end, covered) in value.highlighted) { 97 | val colour = 98 | colorScheme.getAttributes( 99 | if (covered) 100 | CodeInsightColors.LINE_FULL_COVERAGE 101 | else CodeInsightColors.LINE_NONE_COVERAGE 102 | ).foregroundColor 103 | ranges += markupModel.addRangeHighlighter( 104 | editor.logicalPositionToOffset(start), 105 | editor.logicalPositionToOffset(end), 106 | HighlighterLayer.CARET_ROW + 1, 107 | TextAttributes(null, colour, null, EffectType.SEARCH_MATCH, Font.PLAIN), 108 | HighlighterTargetArea.EXACT_RANGE 109 | ) 110 | } 111 | 112 | myActiveInlays.getOrPut(editor) { mutableMapOf() }.getOrPut( 113 | functionGroup 114 | ) { mutableListOf() } += value.branchInfo.map { (startPos, steppedIn, skipped) -> 115 | editor.inlayModel.addInlineElement( 116 | editor.logicalPositionToOffset(startPos), 117 | MyEditorCustomElementRenderer(editor, steppedIn && skipped) 118 | ) 119 | } 120 | } 121 | 122 | private fun applyOnEditor(editor: Editor) { 123 | val vs = FileDocumentManager.getInstance().getFile(editor.document) ?: return 124 | val info = myHighlighting[vs] ?: return 125 | 126 | info.values.forEach { 127 | applyOnHighlightFunction(editor, it) 128 | } 129 | } 130 | 131 | private fun removeFromEditor(editor: Editor) { 132 | myActiveInlays.remove(editor)?.apply { 133 | this.values.flatten().filterNotNull().forEach { 134 | Disposer.dispose(it) 135 | } 136 | } 137 | val markupModel = editor.markupModel as? MarkupModelEx ?: return 138 | myActiveHighlighting.remove(editor)?.apply { 139 | this.values.flatten().filter { 140 | markupModel.containsHighlighter(it) 141 | }.forEach { 142 | markupModel.removeHighlighter(it) 143 | } 144 | } 145 | } 146 | 147 | data class HighlightFunctionGroup( 148 | val region: FunctionRegion, 149 | val functions: Map, 150 | var active: String 151 | ) : Disposable { 152 | override fun dispose() {} 153 | } 154 | 155 | data class HighlightFunction( 156 | val highlighted: List, 157 | val branchInfo: List> 158 | ) 159 | 160 | private val myActiveInlays: MutableMap?>>> = 161 | mutableMapOf() 162 | private val myActiveHighlighting: MutableMap>> = 163 | mutableMapOf() 164 | 165 | private var myHighlighting: Map, HighlightFunctionGroup>> = mapOf() 166 | 167 | val highlighting 168 | get() = synchronized(this) { 169 | myHighlighting 170 | } 171 | 172 | fun changeActive(group: HighlightFunctionGroup, active: String) { 173 | assert(group.functions.contains(active)) 174 | fun changeActiveImpl(editor: Editor?, group: HighlightFunctionGroup, active: String?) { 175 | 176 | val openEditor = editor ?: myActiveHighlighting.entries.find { 177 | it.value.contains(group) 178 | }?.key 179 | if (openEditor != null) { 180 | myActiveInlays[openEditor]?.remove(group)?.filterNotNull()?.forEach { 181 | Disposer.dispose(it) 182 | } 183 | val markupModel = openEditor.markupModel as? MarkupModelEx ?: return 184 | myActiveHighlighting[openEditor]?.remove(group)?.filter { 185 | markupModel.containsHighlighter(it) 186 | }?.forEach { 187 | markupModel.removeHighlighter(it) 188 | } 189 | } 190 | 191 | if (active != null) { 192 | synchronized(this) { 193 | group.active = active 194 | } 195 | } 196 | if (openEditor != null) { 197 | applyOnHighlightFunction(openEditor, group) 198 | if (active != null) { 199 | val vs = FileDocumentManager.getInstance().getFile(openEditor.document) ?: return 200 | myHighlighting[vs]?.values?.filter { 201 | group.region.first < it.region.first && group.region.second > it.region.second 202 | }?.forEach { 203 | changeActiveImpl(openEditor, it, null) 204 | } 205 | } 206 | } 207 | } 208 | changeActiveImpl(null, group, active) 209 | } 210 | 211 | fun setCoverageData(coverageData: CoverageData?) { 212 | synchronized(this) { 213 | myHighlighting.values.forEach { 214 | it.values.forEach { functionGroup -> 215 | Disposer.dispose(functionGroup) 216 | } 217 | } 218 | myHighlighting = mapOf() 219 | clear() 220 | if (coverageData == null) { 221 | myProject.putUserData(DUPLICATE_FUNCTION_GROUP, mutableSetOf()) 222 | DaemonCodeAnalyzer.getInstance(myProject).restart() 223 | return 224 | } 225 | myHighlighting = coverageData.files.mapValues { (_, file) -> 226 | file.functions.values.groupBy { 227 | it.startPos toCP it.endPos 228 | }.map { entry -> 229 | val functions = entry.value.associate { functionData -> 230 | val highlighting = when (functionData.coverage) { 231 | is FunctionLineData -> functionData.coverage.data.map { 232 | Triple( 233 | LogicalPosition(it.key - 1, 0), 234 | LogicalPosition(it.key, 0), 235 | it.value != 0L 236 | ) 237 | } 238 | is FunctionRegionData -> functionData.coverage.data.map { 239 | Triple( 240 | LogicalPosition(it.startPos.first - 1, it.startPos.second - 1), 241 | LogicalPosition(it.endPos.first - 1, it.endPos.second - 1), 242 | it.executionCount != 0L 243 | ) 244 | } 245 | } 246 | val branches = functionData.branches.map { 247 | Triple( 248 | LogicalPosition(it.startPos.first - 1, it.startPos.second - 1), 249 | it.steppedInCount != 0, 250 | it.skippedCount != 0 251 | ) 252 | } 253 | functionData.functionName to HighlightFunction(highlighting, branches) 254 | } 255 | entry.key.first to HighlightFunctionGroup(entry.key, functions, functions.first().key) 256 | }.toMap() 257 | }.mapNotNull { 258 | try { 259 | LocalFileSystem.getInstance().findFileByNioFile(Paths.get(it.key))?.let { vs -> 260 | vs to it.value 261 | } 262 | } catch (e: InvalidPathException) { 263 | null 264 | } 265 | }.toMap() 266 | EditorFactory.getInstance().allEditors.forEach(::applyOnEditor) 267 | myProject.putUserData(DUPLICATE_FUNCTION_GROUP, mutableSetOf()) 268 | DaemonCodeAnalyzer.getInstance(myProject).restart() 269 | } 270 | } 271 | 272 | private fun clear() { 273 | myActiveHighlighting.keys.union(myActiveInlays.keys).forEach(::removeFromEditor) 274 | myActiveInlays.clear() 275 | myActiveHighlighting.clear() 276 | } 277 | 278 | override fun dispose() {} 279 | } 280 | 281 | typealias Region = Triple 282 | 283 | typealias FunctionRegion = ComparablePair, ComparablePair> -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/settings/CoverageGeneratorSettings.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.settings 2 | 3 | import com.intellij.ide.AppLifecycleListener 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.components.* 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.progress.ProcessCanceledException 8 | import com.intellij.util.io.exists 9 | import com.jetbrains.cidr.cpp.toolchains.* 10 | import com.jetbrains.cidr.toolchains.OSType 11 | import net.zero9178.cov.data.CoverageGenerator 12 | import net.zero9178.cov.data.createGeneratorFor 13 | import java.io.File 14 | import java.nio.file.Path 15 | import java.nio.file.Paths 16 | 17 | @State( 18 | name = "net.zero9178.coverage.settings", 19 | storages = [Storage("zero9178.coverage.xml", roamingType = RoamingType.DISABLED)] 20 | ) 21 | class CoverageGeneratorSettings : PersistentStateComponent { 22 | 23 | data class GeneratorInfo( 24 | var gcovOrllvmCovPath: String = "", 25 | var llvmProfDataPath: String? = null, 26 | var demangler: String? = null 27 | ) { 28 | fun copy() = GeneratorInfo(gcovOrllvmCovPath, llvmProfDataPath, demangler) 29 | } 30 | 31 | data class State( 32 | var paths: MutableMap = mutableMapOf(), 33 | var ifBranchCoverageEnabled: Boolean = true, 34 | var loopBranchCoverageEnabled: Boolean = true, 35 | var conditionalExpBranchCoverageEnabled: Boolean = true, 36 | var booleanOpBranchCoverageEnabled: Boolean = true, 37 | var branchCoverageEnabled: Boolean = true, 38 | var useCoverageAction: Boolean = true, 39 | var calculateExternalSources: Boolean = false 40 | ) 41 | 42 | private var myState: State = State() 43 | 44 | private var myGenerators: MutableMap> = mutableMapOf() 45 | 46 | var paths: Map 47 | get() = myState.paths 48 | set(value) { 49 | myState.paths = value.toMutableMap() 50 | myGenerators.clear() 51 | myState.paths.forEach { 52 | generateGeneratorFor(it.key, it.value) 53 | } 54 | } 55 | 56 | var ifBranchCoverageEnabled: Boolean 57 | get() = myState.ifBranchCoverageEnabled 58 | set(value) { 59 | myState.ifBranchCoverageEnabled = value 60 | } 61 | 62 | var loopBranchCoverageEnabled: Boolean 63 | get() = myState.loopBranchCoverageEnabled 64 | set(value) { 65 | myState.loopBranchCoverageEnabled = value 66 | } 67 | 68 | var booleanOpBranchCoverageEnabled: Boolean 69 | get() = myState.booleanOpBranchCoverageEnabled 70 | set(value) { 71 | myState.booleanOpBranchCoverageEnabled = value 72 | } 73 | 74 | var conditionalExpCoverageEnabled: Boolean 75 | get() = myState.conditionalExpBranchCoverageEnabled 76 | set(value) { 77 | myState.conditionalExpBranchCoverageEnabled = value 78 | } 79 | 80 | var branchCoverageEnabled: Boolean 81 | get() = myState.branchCoverageEnabled 82 | set(value) { 83 | myState.branchCoverageEnabled = value 84 | } 85 | 86 | var calculateExternalSources: Boolean 87 | get() = myState.calculateExternalSources 88 | set(value) { 89 | myState.calculateExternalSources = value 90 | } 91 | 92 | fun getGeneratorFor(toolchain: String) = myGenerators[toolchain] 93 | 94 | override fun getState() = myState 95 | 96 | override fun loadState(state: State) { 97 | val paths = myState.paths 98 | myState = state 99 | myState.paths.forEach { 100 | if (paths.contains(it.key)) { 101 | paths[it.key] = it.value 102 | } 103 | } 104 | myState.paths = paths 105 | ensurePopulatedPaths() 106 | } 107 | 108 | private fun ensurePopulatedPaths() { 109 | ApplicationManager.getApplication().executeOnPooledThread { 110 | if (paths.isEmpty()) { 111 | paths = CPPToolchains.getInstance().toolchains.associateBy({ it.name }, { 112 | guessCoverageGeneratorForToolchain(it) 113 | }).toMutableMap() 114 | } else { 115 | paths = paths.mapValues { 116 | if (it.value.gcovOrllvmCovPath.isNotEmpty()) { 117 | it.value 118 | } else { 119 | val toolchain = 120 | CPPToolchains.getInstance().toolchains.find { toolchain -> toolchain.name == it.key } 121 | ?: return@mapValues GeneratorInfo() 122 | guessCoverageGeneratorForToolchain(toolchain) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | private fun generateGeneratorFor(name: String, info: GeneratorInfo) { 130 | myGenerators[name] = createGeneratorFor( 131 | info.gcovOrllvmCovPath, 132 | info.llvmProfDataPath, 133 | info.demangler, 134 | CPPToolchains.getInstance().toolchains.find { it.name == name }?.wsl 135 | ) 136 | } 137 | 138 | init { 139 | ApplicationManager.getApplication().messageBus.connect() 140 | .subscribe(CPPToolchainsListener.TOPIC, object : CPPToolchainsListener { 141 | override fun toolchainsRenamed(renamed: MutableMap) { 142 | for (renames in renamed) { 143 | val value = myState.paths.remove(renames.key) 144 | if (value != null) { 145 | myState.paths[renames.value] = value 146 | } 147 | val generator = myGenerators.remove(renames.key) 148 | if (generator != null) { 149 | myGenerators[renames.value] = generator 150 | } 151 | } 152 | } 153 | 154 | override fun toolchainCMakeEnvironmentChanged(toolchains: MutableSet) { 155 | try { 156 | toolchains.groupBy { 157 | CPPToolchains.getInstance().toolchains.contains(it) 158 | }.forEach { group -> 159 | if (group.key) { 160 | group.value.forEach { 161 | val path = guessCoverageGeneratorForToolchain(it) 162 | myState.paths[it.name] = path 163 | generateGeneratorFor(it.name, path) 164 | } 165 | } else { 166 | group.value.forEach { 167 | myState.paths.remove(it.name) 168 | myGenerators.remove(it.name) 169 | } 170 | } 171 | } 172 | } catch (e: ProcessCanceledException) { 173 | throw e 174 | } catch (e: Exception) { 175 | log.error(e) 176 | } 177 | } 178 | }) 179 | ensurePopulatedPaths() 180 | } 181 | 182 | companion object { 183 | fun getInstance() = service() 184 | 185 | val log = Logger.getInstance(CoverageGeneratorSettings::class.java) 186 | } 187 | 188 | class Registrator : AppLifecycleListener { 189 | override fun appFrameCreated(commandLineArgs: MutableList) { 190 | getInstance() 191 | } 192 | } 193 | } 194 | 195 | private fun guessCoverageGeneratorForToolchain(toolchain: CPPToolchains.Toolchain): CoverageGeneratorSettings.GeneratorInfo { 196 | val toolset = toolchain.toolSet 197 | var compiler = 198 | toolchain.customCXXCompilerPath 199 | ?: if (toolset !is WSL) System.getenv("CXX")?.ifBlank { System.getenv("CC") } else null 200 | val findExe = { prefix: String, name: String, suffix: String, extraPath: Path? -> 201 | val insideSameDir = extraPath?.toFile()?.listFiles()?.asSequence()?.map { 202 | "($prefix)?$name($suffix)?".toRegex().matchEntire(it.name) 203 | }?.filterNotNull()?.maxByOrNull { 204 | it.value.length 205 | }?.value 206 | when { 207 | insideSameDir != null -> extraPath.resolve(insideSameDir).toString() 208 | toolset !is WSL -> { 209 | val pair = System.getenv("PATH").splitToSequence(File.pathSeparatorChar).asSequence().map { 210 | Paths.get(it).toFile() 211 | }.map { path -> 212 | val result = path.listFiles()?.asSequence()?.map { 213 | it to "($prefix)?$name($suffix)?".toRegex().matchEntire(it.name) 214 | }?.filter { it.second != null }?.map { it.first to it.second!! } 215 | ?.maxByOrNull { it.second.value.length } 216 | result 217 | }.filterNotNull().maxByOrNull { it.second.value.length } 218 | pair?.first?.absolutePath 219 | } 220 | else -> null 221 | } 222 | } 223 | 224 | if (compiler != null && compiler.contains("clang", true)) { 225 | //We are using clang so we need to look for llvm-cov. We are first going to check if 226 | //llvm-cov is next to the compiler. If not we looking for it on PATH 227 | 228 | val compilerName = Paths.get(compiler).fileName.toString() 229 | 230 | val clangName = when { 231 | compilerName.contains("clang++") -> "clang++" 232 | compilerName.contains("clang-cl") -> "clang-cl" 233 | else -> "clang" 234 | } 235 | 236 | val prefix = compilerName.substringBefore(clangName) 237 | 238 | val suffix = compilerName.substringAfter(clangName) 239 | 240 | val covPath = findExe( 241 | prefix, 242 | "llvm-cov", 243 | suffix, 244 | Paths.get(if (toolset is WSL) toolset.toLocalPath(null, compiler) else compiler).parent 245 | ) 246 | 247 | val profPath = findExe( 248 | prefix, 249 | "llvm-profdata", 250 | suffix, 251 | Paths.get(if (toolset is WSL) toolset.toLocalPath(null, compiler) else compiler).parent 252 | ) 253 | 254 | val finalFilt = if (toolset !is MSVC) { 255 | val llvmFilt = findExe( 256 | prefix, 257 | "llvm-cxxfilt", 258 | suffix, 259 | Paths.get(if (toolset is WSL) toolset.toLocalPath(null, compiler) else compiler).parent 260 | ) 261 | 262 | llvmFilt ?: findExe( 263 | prefix, 264 | "c++filt", 265 | suffix, 266 | Paths.get(if (toolset is WSL) toolset.toLocalPath(null, compiler) else compiler).parent 267 | ) 268 | } else { 269 | findExe( 270 | prefix, 271 | "llvm-undname", 272 | suffix, 273 | Paths.get(compiler).parent 274 | ) 275 | } 276 | 277 | return if (profPath == null || covPath == null) { 278 | CoverageGeneratorSettings.GeneratorInfo() 279 | } else { 280 | CoverageGeneratorSettings.GeneratorInfo( 281 | if (toolset is WSL) toolset.toEnvPath(covPath) else covPath, 282 | if (toolset is WSL) toolset.toEnvPath(profPath) else profPath, 283 | if (finalFilt != null) { 284 | if (toolset is WSL) toolset.toEnvPath(finalFilt) else finalFilt 285 | } else null 286 | ) 287 | } 288 | } else if (compiler == null) { 289 | if (toolset is MSVC) { 290 | // If the toolset is MSVC but the compiler isn't clang we have no chance. We are done here 291 | return CoverageGeneratorSettings.GeneratorInfo() 292 | } 293 | if (toolset is MinGW) { 294 | val path = toolset.home.toPath().resolve("bin") 295 | .resolve(if (OSType.getCurrent() == OSType.WIN) "gcov.exe" else "gcov") 296 | return if (path.exists()) { 297 | CoverageGeneratorSettings.GeneratorInfo(path.toString()) 298 | } else { 299 | CoverageGeneratorSettings.GeneratorInfo() 300 | } 301 | } 302 | compiler = "/usr/bin/gcc" 303 | } 304 | 305 | val compilerName = Paths.get(compiler).fileName.toString() 306 | 307 | val gccName = if (compilerName.contains("g++")) "g++" else "gcc" 308 | 309 | val prefix = compilerName.substringBefore(gccName) 310 | 311 | val suffix = compilerName.substringAfter(gccName) 312 | 313 | val gcovPath = findExe( 314 | prefix, 315 | "gcov", 316 | suffix, 317 | Paths.get(if (toolset is WSL) toolset.toLocalPath(null, compiler) else compiler).parent 318 | ) 319 | return if (gcovPath != null) { 320 | CoverageGeneratorSettings.GeneratorInfo(if (toolset is WSL) toolset.toEnvPath(gcovPath) else gcovPath) 321 | } else { 322 | CoverageGeneratorSettings.GeneratorInfo() 323 | } 324 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/CoverageConfigurationExtension.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov 2 | 3 | import com.intellij.execution.configurations.GeneralCommandLine 4 | import com.intellij.execution.configurations.RunnerSettings 5 | import com.intellij.execution.process.ProcessAdapter 6 | import com.intellij.execution.process.ProcessEvent 7 | import com.intellij.execution.process.ProcessHandler 8 | import com.intellij.notification.NotificationGroupManager 9 | import com.intellij.notification.NotificationType 10 | import com.intellij.openapi.application.invokeAndWaitIfNeeded 11 | import com.intellij.openapi.application.invokeLater 12 | import com.intellij.openapi.progress.PerformInBackgroundOption 13 | import com.intellij.openapi.progress.ProgressIndicator 14 | import com.intellij.openapi.progress.ProgressManager 15 | import com.intellij.openapi.progress.Task 16 | import com.intellij.openapi.project.Project 17 | import com.intellij.openapi.roots.ModuleRootManager 18 | import com.intellij.openapi.vfs.VirtualFile 19 | import com.intellij.openapi.wm.ToolWindowManager 20 | import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace 21 | import com.jetbrains.cidr.cpp.execution.CMakeAppRunConfiguration 22 | import com.jetbrains.cidr.cpp.execution.CMakeBuildProfileExecutionTarget 23 | import com.jetbrains.cidr.cpp.execution.coverage.CMakeCoverageBuildOptionsInstallerFactory 24 | import com.jetbrains.cidr.cpp.toolchains.CPPEnvironment 25 | import com.jetbrains.cidr.execution.CidrBuildTarget 26 | import com.jetbrains.cidr.execution.CidrRunConfiguration 27 | import com.jetbrains.cidr.execution.CidrRunConfigurationExtensionBase 28 | import com.jetbrains.cidr.execution.ConfigurationExtensionContext 29 | import com.jetbrains.cidr.lang.CLanguageKind 30 | import com.jetbrains.cidr.lang.toolchains.CidrToolEnvironment 31 | import net.zero9178.cov.actions.RunCoverageSettings 32 | import net.zero9178.cov.data.* 33 | import net.zero9178.cov.editor.CoverageFileAccessProtector 34 | import net.zero9178.cov.editor.CoverageHighlighter 35 | import net.zero9178.cov.settings.CoverageGeneratorSettings 36 | import net.zero9178.cov.util.getCMakeConfigurations 37 | import net.zero9178.cov.window.CoverageView 38 | import javax.swing.event.HyperlinkEvent 39 | import javax.swing.tree.DefaultMutableTreeNode 40 | 41 | class CoverageConfigurationExtension : CidrRunConfigurationExtensionBase() { 42 | 43 | override fun isApplicableFor(configuration: CidrRunConfiguration<*, *>) = true 44 | 45 | override fun isEnabledFor( 46 | applicableConfiguration: CidrRunConfiguration<*, out CidrBuildTarget<*>>, 47 | environment: CidrToolEnvironment, 48 | runnerSettings: RunnerSettings? 49 | ): Boolean { 50 | if (environment !is CPPEnvironment) { 51 | return false 52 | } 53 | return runnerSettings is RunCoverageSettings 54 | } 55 | 56 | override fun patchCommandLine( 57 | configuration: CidrRunConfiguration<*, out CidrBuildTarget<*>>, 58 | runnerSettings: RunnerSettings?, 59 | environment: CidrToolEnvironment, 60 | cmdLine: GeneralCommandLine, 61 | runnerId: String, 62 | context: ConfigurationExtensionContext 63 | ) { 64 | if (environment !is CPPEnvironment || configuration !is CMakeAppRunConfiguration || runnerSettings !is RunCoverageSettings) { 65 | return 66 | } 67 | getCoverageGenerator(environment, configuration.project)?.patchEnvironment( 68 | configuration, 69 | environment, 70 | runnerSettings.executionTarget, 71 | cmdLine, 72 | context 73 | ) 74 | } 75 | 76 | override fun attachToProcess( 77 | configuration: CidrRunConfiguration<*, out CidrBuildTarget<*>>, 78 | handler: ProcessHandler, 79 | environment: CidrToolEnvironment, 80 | runnerSettings: RunnerSettings?, 81 | runnerId: String, 82 | context: ConfigurationExtensionContext 83 | ) { 84 | if (environment !is CPPEnvironment || configuration !is CMakeAppRunConfiguration || runnerSettings !is RunCoverageSettings) { 85 | return 86 | } 87 | handler.addProcessListener(object : ProcessAdapter() { 88 | override fun processTerminated(event: ProcessEvent) { 89 | if (!hasCompilerFlags(configuration, runnerSettings.executionTarget)) { 90 | val factory = CMakeCoverageBuildOptionsInstallerFactory() 91 | val installer = factory.getInstaller(configuration) 92 | if (installer != null && installer.canInstall(configuration, configuration.project)) { 93 | NotificationGroupManager.getInstance().getNotificationGroup("C/C++ Coverage Notification") 94 | .createNotification( 95 | "Missing compilation flags", 96 | "Compiler flags for generating coverage are missing.\n" 97 | + "Would you like to create a new profile with them now?\nCreate", NotificationType.ERROR 99 | ).setListener { _, hyperlinkEvent -> 100 | if (hyperlinkEvent.eventType == HyperlinkEvent.EventType.ACTIVATED) { 101 | if (!installer.install( 102 | {}, 103 | configuration, 104 | configuration.project 105 | ) 106 | ) { 107 | NotificationGroupManager.getInstance() 108 | .getNotificationGroup("C/C++ Coverage Notification") 109 | .createNotification( 110 | "Failed to add compiler flags", NotificationType.ERROR 111 | ).notify(configuration.project) 112 | } 113 | } 114 | }.notify(configuration.project) 115 | } 116 | } else { 117 | ProgressManager.getInstance() 118 | .run(object : Task.Backgroundable( 119 | configuration.project, "Gathering coverage...", true, 120 | PerformInBackgroundOption.DEAF 121 | ) { 122 | override fun run(indicator: ProgressIndicator) { 123 | indicator.isIndeterminate = true 124 | 125 | if (!ProjectSemaphore.getInstance(project).semaphore.tryAcquire()) { 126 | indicator.text = "Waiting for other coverage gathering to finish" 127 | ProjectSemaphore.getInstance(project).semaphore.acquire() 128 | indicator.text = "" 129 | } 130 | 131 | val coverageGenerator = getCoverageGenerator(environment, configuration.project) 132 | val needsSourcefiles = when (coverageGenerator) { 133 | is LLVMCoverageGenerator -> coverageGenerator.majorVersion < 12 134 | is GCCJSONCoverageGenerator -> true 135 | is GCCGCDACoverageGenerator -> false 136 | else -> false 137 | } 138 | if (needsSourcefiles) { 139 | synchronized(CoverageHighlighter.getInstance(project)) { 140 | val sources = getSourceFiles(project) 141 | invokeLater { 142 | CoverageFileAccessProtector.currentlyInCoverage[configuration.project] = 143 | sources 144 | } 145 | } 146 | } 147 | val data = 148 | coverageGenerator?.generateCoverage( 149 | configuration, 150 | environment, 151 | runnerSettings.executionTarget, 152 | indicator, 153 | context 154 | ) 155 | val root = DefaultMutableTreeNode("invisible-root") 156 | invokeLater { 157 | CoverageHighlighter.getInstance(configuration.project).setCoverageData(data) 158 | } 159 | if (data != null) { 160 | createCoverageViewTree(root, data) 161 | } 162 | invokeLater { 163 | CoverageView.getInstance(configuration.project) 164 | .setRoot( 165 | root, 166 | data?.hasBranchCoverage ?: true, 167 | data?.containsExternalSources ?: false 168 | ) 169 | ToolWindowManager.getInstance(configuration.project).getToolWindow("C/C++ Coverage") 170 | ?.show(null) 171 | } 172 | } 173 | 174 | override fun onFinished() { 175 | invokeAndWaitIfNeeded { 176 | CoverageFileAccessProtector.currentlyInCoverage.remove(configuration.project) 177 | } 178 | ProjectSemaphore.getInstance(project).semaphore.release() 179 | } 180 | }) 181 | } 182 | } 183 | }) 184 | } 185 | 186 | private fun getCoverageGenerator( 187 | environment: CPPEnvironment, 188 | project: Project 189 | ): CoverageGenerator? { 190 | val generator = CoverageGeneratorSettings.getInstance().getGeneratorFor(environment.toolchain.name) 191 | if (generator == null) { 192 | NotificationGroupManager.getInstance().getNotificationGroup("C/C++ Coverage Notification") 193 | .createNotification( 194 | "Neither gcov nor llvm-cov specified for ${environment.toolchain.name}", 195 | NotificationType.ERROR 196 | ).notify(project) 197 | return null 198 | } 199 | val coverageGenerator = generator.first 200 | if (coverageGenerator == null) { 201 | if (generator.second != null) { 202 | NotificationGroupManager.getInstance().getNotificationGroup("C/C++ Coverage Notification") 203 | .createNotification( 204 | "Coverage could not be generated due to following error: ${generator.second}", 205 | NotificationType.ERROR 206 | ).notify(project) 207 | } 208 | return null 209 | } 210 | return coverageGenerator 211 | } 212 | 213 | private fun hasCompilerFlags( 214 | configuration: CMakeAppRunConfiguration, 215 | executionTarget: CMakeBuildProfileExecutionTarget 216 | ) = getCMakeConfigurations(configuration, executionTarget).any { 217 | CLanguageKind.values().any { kind -> 218 | val list = it.getCombinedCompilerFlags(kind, null) 219 | list.contains("--coverage") || list.containsAll( 220 | listOf( 221 | "-fprofile-arcs", 222 | "-ftest-coverage" 223 | ) 224 | ) || (list.contains("-fcoverage-mapping") && list.any { 225 | it.matches("-fprofile-instr-generate(=.*)?".toRegex()) 226 | }) 227 | } 228 | } 229 | 230 | private fun getSourceFiles( 231 | project: Project 232 | ): Set { 233 | return CMakeWorkspace.getInstance(project).module?.let { module -> 234 | ModuleRootManager.getInstance(module).contentEntries.map { 235 | it.sourceFolderFiles.toSet() 236 | }.fold(emptySet()) { result, curr -> 237 | result.union(curr) 238 | } 239 | } ?: emptySet() 240 | } 241 | 242 | private fun createCoverageViewTree(root: DefaultMutableTreeNode, data: CoverageData) { 243 | 244 | data class ChosenName(val filepath: String, var count: Int, val data: CoverageFileData) { 245 | fun getFilename(): String { 246 | return filepath.split('/').takeLast(count).joinToString("/") 247 | } 248 | } 249 | 250 | val previouslyChanged = mutableSetOf() 251 | val map = mutableMapOf() 252 | for ((_, value) in data.files) { 253 | var new = ChosenName(value.filePath.replace('\\', '/'), 1, value) 254 | val filename = new.getFilename() 255 | var existing = map[filename] 256 | if (existing == null && !previouslyChanged.contains(filename)) { 257 | map[filename] = new 258 | continue 259 | } 260 | map.remove(filename) 261 | while (new.getFilename() == existing?.getFilename() || previouslyChanged.contains(new.getFilename())) { 262 | previouslyChanged.add(new.getFilename()) 263 | new = new.copy(count = new.count + 1) 264 | existing = existing?.copy(count = existing.count + 1) 265 | if (existing == null) { 266 | // If existing is null due to a filename being part of previouslyChanged, also attempt to find 267 | // existing files that might also have to be changed a long the way 268 | existing = map[new.getFilename()] 269 | } 270 | } 271 | map[new.getFilename()] = new 272 | existing?.let { 273 | map[it.getFilename()] = it 274 | } 275 | } 276 | 277 | val fileDataToName = map.map { 278 | it.value.data to it.key 279 | }.toMap() 280 | 281 | for ((_, value) in data.files) { 282 | val fileNode = object : DefaultMutableTreeNode(value) { 283 | override fun toString(): String { 284 | val coverageFileData = userObject as? CoverageFileData ?: return super.toString() 285 | return fileDataToName[coverageFileData] ?: return super.toString() 286 | } 287 | } 288 | 289 | root.add(fileNode) 290 | for (function in value.functions.values) { 291 | fileNode.add(object : DefaultMutableTreeNode(function) { 292 | override fun toString() = 293 | (userObject as? CoverageFunctionData)?.functionName 294 | ?: userObject.toString() 295 | }) 296 | } 297 | } 298 | } 299 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/zero9178/cov/window/CoverageViewImpl.kt: -------------------------------------------------------------------------------- 1 | package net.zero9178.cov.window 2 | 3 | import com.intellij.openapi.editor.colors.CodeInsightColors 4 | import com.intellij.openapi.editor.colors.EditorColorsUtil 5 | import com.intellij.openapi.fileEditor.FileEditorManager 6 | import com.intellij.openapi.fileEditor.OpenFileDescriptor 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.roots.ModuleRootManager 9 | import com.intellij.openapi.vfs.LocalFileSystem 10 | import com.intellij.openapi.vfs.VfsUtil 11 | import com.intellij.openapi.vfs.VirtualFile 12 | import com.intellij.ui.JBColor 13 | import com.intellij.ui.SpeedSearchComparator 14 | import com.intellij.ui.TreeTableSpeedSearch 15 | import com.intellij.ui.dualView.TreeTableView 16 | import com.intellij.ui.treeStructure.treetable.ListTreeTableModelOnColumns 17 | import com.intellij.ui.treeStructure.treetable.TreeColumnInfo 18 | import com.intellij.util.ui.ColumnInfo 19 | import com.intellij.util.ui.UIUtil 20 | import com.intellij.util.ui.tree.TreeUtil 21 | import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace 22 | import net.zero9178.cov.data.CoverageFileData 23 | import net.zero9178.cov.data.CoverageFunctionData 24 | import net.zero9178.cov.data.FunctionLineData 25 | import net.zero9178.cov.data.FunctionRegionData 26 | import net.zero9178.cov.editor.CoverageHighlighter 27 | import net.zero9178.cov.settings.CoverageGeneratorSettings 28 | import java.awt.Component 29 | import java.awt.Graphics 30 | import java.awt.event.MouseAdapter 31 | import java.awt.event.MouseEvent 32 | import java.awt.event.MouseEvent.BUTTON1 33 | import java.nio.file.Paths 34 | import javax.swing.* 35 | import javax.swing.table.TableCellRenderer 36 | import javax.swing.tree.DefaultMutableTreeNode 37 | import javax.swing.tree.DefaultTreeModel 38 | import javax.swing.tree.TreeNode 39 | 40 | class CoverageViewImpl(val project: Project) : CoverageView() { 41 | 42 | private fun sort( 43 | ascending: Boolean, 44 | fileComparator: (CoverageFileData, CoverageFileData) -> Int, 45 | functionComparator: (CoverageFunctionData, CoverageFunctionData) -> Int 46 | ) = 47 | TreeUtil.sort(myTreeTableView.tableModel as DefaultTreeModel) { lhs: Any, rhs: Any -> 48 | lhs as DefaultMutableTreeNode 49 | rhs as DefaultMutableTreeNode 50 | val lhsUS = lhs.userObject 51 | val rhsUS = rhs.userObject 52 | if (lhsUS is CoverageFileData && rhsUS is CoverageFileData) { 53 | if (ascending) { 54 | fileComparator(lhsUS, rhsUS) 55 | } else { 56 | fileComparator(rhsUS, lhsUS) 57 | } 58 | } else if (lhsUS is CoverageFunctionData && rhsUS is CoverageFunctionData) { 59 | if (ascending) { 60 | functionComparator(lhsUS, rhsUS) 61 | } else { 62 | functionComparator(rhsUS, lhsUS) 63 | } 64 | } else { 65 | 0 66 | } 67 | } 68 | 69 | private class FunctionComparator(private val col: Int) : (CoverageFunctionData, CoverageFunctionData) -> Int { 70 | override operator fun invoke(lhs: CoverageFunctionData, rhs: CoverageFunctionData) = 71 | when (col) { 72 | 0 -> lhs.functionName.compareTo(rhs.functionName) 73 | 1 -> getBranchCoverage(lhs).compareTo(getBranchCoverage(rhs)) 74 | else -> (getCurrentLineCoverage(lhs).toDouble() / getMaxLineCoverage(lhs)).compareTo( 75 | (getCurrentLineCoverage( 76 | rhs 77 | ).toDouble() / getMaxLineCoverage(rhs)) 78 | ) 79 | } 80 | } 81 | 82 | private class FileComparator(private val col: Int) : (CoverageFileData, CoverageFileData) -> Int { 83 | override fun invoke(lhs: CoverageFileData, rhs: CoverageFileData) = when (col) { 84 | 0 -> lhs.filePath.compareTo(rhs.filePath) 85 | 1 -> getBranchCoverage(lhs).compareTo(getBranchCoverage(rhs)) 86 | else -> (getCurrentLineCoverage(lhs).toDouble() / getMaxLineCoverage(lhs)).compareTo( 87 | (getCurrentLineCoverage( 88 | rhs 89 | ).toDouble() / getMaxLineCoverage(rhs)) 90 | ) 91 | } 92 | } 93 | 94 | private var mySorting = MutableList(myTreeTableView.columnCount) { 95 | SortOrder.UNSORTED 96 | } 97 | 98 | override fun createUIComponents() { 99 | myTreeTableView = 100 | TreeTableView(ListTreeTableModelOnColumns(DefaultMutableTreeNode("empty-root"), getColumnInfo(true))) 101 | myTreeTableView.rowSelectionAllowed = true 102 | myTreeTableView.addMouseListener(object : MouseAdapter() { 103 | override fun mouseClicked(e: MouseEvent?) { 104 | e ?: return 105 | if (e.clickCount != 2 || e.button != BUTTON1) { 106 | return 107 | } 108 | 109 | val selRow = myTreeTableView.rowAtPoint(e.point) 110 | val selColumn = myTreeTableView.columnAtPoint(e.point) 111 | if (selRow < 0 || selColumn != 0) { 112 | return 113 | } 114 | 115 | val node = myTreeTableView.getValueAt(selRow, selColumn) as DefaultMutableTreeNode 116 | when (val data = node.userObject) { 117 | is CoverageFileData -> { 118 | val file = VfsUtil.findFileByIoFile(Paths.get(data.filePath).toFile(), true) ?: return 119 | 120 | FileEditorManager.getInstance(project).openEditor(OpenFileDescriptor(project, file), true) 121 | } 122 | is CoverageFunctionData -> { 123 | val fileData = (node.parent as DefaultMutableTreeNode).userObject as CoverageFileData 124 | val file = VfsUtil.findFileByIoFile(Paths.get(fileData.filePath).toFile(), true) ?: return 125 | 126 | FileEditorManager.getInstance(project) 127 | .openEditor( 128 | OpenFileDescriptor( 129 | project, 130 | file, 131 | data.startPos.first - 1, 132 | data.startPos.second 133 | ), true 134 | ) 135 | } 136 | } 137 | } 138 | }) 139 | myTreeTableView.tableHeader.addMouseListener(object : MouseAdapter() { 140 | override fun mouseClicked(e: MouseEvent?) { 141 | e ?: return 142 | if (e.button != BUTTON1) { 143 | return 144 | } 145 | myTreeTableView.clearSelection() 146 | val col = myTreeTableView.columnAtPoint(e.point) 147 | if (col > mySorting.lastIndex) { 148 | return 149 | } 150 | 151 | mySorting[col] = when (mySorting[col]) { 152 | SortOrder.ASCENDING -> { 153 | sort(false, FileComparator(col), FunctionComparator(col)) 154 | SortOrder.DESCENDING 155 | } 156 | SortOrder.DESCENDING, SortOrder.UNSORTED -> { 157 | sort(true, FileComparator(col), FunctionComparator(col)) 158 | SortOrder.ASCENDING 159 | } 160 | } 161 | for (i in 0..mySorting.lastIndex) { 162 | if (i == col) { 163 | continue 164 | } else { 165 | mySorting[i] = SortOrder.UNSORTED 166 | } 167 | } 168 | myTreeTableView.updateUI() 169 | } 170 | }) 171 | val defaultRenderer = myTreeTableView.tableHeader.defaultRenderer 172 | myTreeTableView.tableHeader.defaultRenderer = 173 | TableCellRenderer { table, value, isSelected, hasFocus, row, column -> 174 | val c = defaultRenderer.getTableCellRendererComponent( 175 | table, 176 | value, 177 | isSelected, 178 | hasFocus, 179 | row, 180 | column 181 | ) 182 | if (column > mySorting.lastIndex || c !is JLabel) { 183 | c 184 | } else { 185 | c.icon = when (mySorting[column]) { 186 | SortOrder.ASCENDING -> UIManager.getIcon("Table.ascendingSortIcon") 187 | SortOrder.DESCENDING -> UIManager.getIcon("Table.descendingSortIcon") 188 | SortOrder.UNSORTED -> null 189 | } 190 | c 191 | } 192 | } 193 | TreeTableSpeedSearch(myTreeTableView).comparator = SpeedSearchComparator(false) 194 | } 195 | 196 | init { 197 | myClear.addActionListener { 198 | CoverageHighlighter.getInstance(project).setCoverageData(null) 199 | setRoot( 200 | null, 201 | CoverageGeneratorSettings.getInstance().branchCoverageEnabled, 202 | CoverageGeneratorSettings.getInstance().calculateExternalSources 203 | ) 204 | } 205 | myIncludeNonProjectSources.addActionListener { 206 | (myTreeTableView.tableModel as DefaultTreeModel).reload() 207 | } 208 | } 209 | 210 | override fun setRoot(treeNode: DefaultMutableTreeNode?, hasBranchCoverage: Boolean, hasExternalSources: Boolean) { 211 | val list = TreeUtil.collectExpandedUserObjects(myTreeTableView.tree).filterIsInstance() 212 | myTreeTableView.setModel( 213 | object : ListTreeTableModelOnColumns( 214 | treeNode ?: DefaultMutableTreeNode("empty-root"), 215 | getColumnInfo(hasBranchCoverage) 216 | ) { 217 | override fun getChildCount(parent: Any?): Int { 218 | return if (myIncludeNonProjectSources.isSelected || parent == null || parent !is DefaultMutableTreeNode || !parent.isRoot) { 219 | super.getChildCount(parent) 220 | } else { 221 | var count = 0 222 | for (i in 0 until parent.childCount) { 223 | val child = parent.getChildAt(i) 224 | if (isProjectSource(child)) { 225 | count++ 226 | } 227 | } 228 | count 229 | } 230 | } 231 | 232 | override fun getChild(parent: Any?, index: Int): Any { 233 | if (myIncludeNonProjectSources.isSelected || parent == null || parent !is DefaultMutableTreeNode || !parent.isRoot) { 234 | return super.getChild(parent, index) 235 | } else { 236 | var count = 0 237 | for (i in 0 until parent.childCount) { 238 | val child = parent.getChildAt(i) 239 | if (isProjectSource(child)) { 240 | if (count == index) { 241 | return child 242 | } 243 | count++ 244 | } 245 | } 246 | return super.getChild(parent, index) 247 | } 248 | } 249 | 250 | private fun isProjectSource(child: TreeNode): Boolean { 251 | if (child !is DefaultMutableTreeNode) { 252 | return true 253 | } 254 | val coverageFileData = child.userObject as? CoverageFileData ?: return true 255 | val vs = LocalFileSystem.getInstance().findFileByNioFile(Paths.get(coverageFileData.filePath)) 256 | ?: return true 257 | val projectSources = CMakeWorkspace.getInstance(project).module?.let { module -> 258 | ModuleRootManager.getInstance(module).contentEntries.map { 259 | it.sourceFolderFiles.toSet() 260 | }.fold(emptySet()) { result, curr -> 261 | result.union(curr) 262 | } 263 | } ?: emptySet() 264 | return projectSources.contains(vs) 265 | } 266 | } 267 | ) 268 | fun doFunctionSelection(item: DefaultMutableTreeNode) { 269 | val coverageFunctionData = item.userObject as? CoverageFunctionData ?: return 270 | val fileData = (item.parent as? DefaultMutableTreeNode)?.userObject as? CoverageFileData 271 | ?: return 272 | val vs = LocalFileSystem.getInstance().findFileByNioFile(Paths.get(fileData.filePath)) 273 | ?: return 274 | val highlighter = CoverageHighlighter.getInstance(project) 275 | val file = highlighter.highlighting[vs] ?: return 276 | val group = file[coverageFunctionData.startPos] ?: return 277 | if (group.functions.size <= 1) { 278 | return 279 | } 280 | highlighter.changeActive(group, coverageFunctionData.functionName) 281 | } 282 | 283 | myTreeTableView.selectionModel.addListSelectionListener { 284 | if (myTreeTableView.selectedRowCount != 1) { 285 | return@addListSelectionListener 286 | } 287 | val item = 288 | myTreeTableView.model.getValueAt(myTreeTableView.selectedRow, 0) as? DefaultMutableTreeNode 289 | ?: return@addListSelectionListener 290 | doFunctionSelection(item) 291 | } 292 | TreeUtil.treeNodeTraverser(treeNode).forEach { node -> 293 | if (node !is DefaultMutableTreeNode) return@forEach 294 | val file = node.userObject as? CoverageFileData ?: return@forEach 295 | if (list.any { it.filePath == file.filePath }) { 296 | myTreeTableView.tree.expandPath(TreeUtil.getPathFromRoot(node)) 297 | } 298 | } 299 | if (mySorting.size != myTreeTableView.columnCount) { 300 | mySorting = MutableList(myTreeTableView.columnCount) { 301 | SortOrder.UNSORTED 302 | } 303 | } 304 | myTreeTableView.setRootVisible(false) 305 | myTreeTableView.setMinRowHeight((myTreeTableView.font.size * 1.75).toInt()) 306 | loop@ for (i in 0 until myTreeTableView.columnCount) { 307 | myTreeTableView.clearSelection() 308 | when (mySorting[i]) { 309 | SortOrder.UNSORTED -> continue@loop 310 | SortOrder.ASCENDING -> { 311 | sort(true, FileComparator(i), FunctionComparator(i)) 312 | } 313 | SortOrder.DESCENDING -> { 314 | sort(false, FileComparator(i), FunctionComparator(i)) 315 | } 316 | } 317 | myTreeTableView.updateUI() 318 | } 319 | myIncludeNonProjectSources.isVisible = hasExternalSources 320 | } 321 | } 322 | 323 | private class CoverageBar : JPanel() { 324 | 325 | init { 326 | isOpaque = false 327 | border = BorderFactory.createEmptyBorder(2, 2, 2, 2) 328 | font = UIUtil.getLabelFont() 329 | } 330 | 331 | var text: String = "" 332 | private set 333 | 334 | private fun updateText() { 335 | if (max != 0L) { 336 | text = (100.0 * current / max).toInt().toString() + "%" 337 | toolTipText = "$current/$max" 338 | } else { 339 | text = "N/A" 340 | toolTipText = text 341 | } 342 | } 343 | 344 | var max: Long = 100 345 | set(value) { 346 | field = value 347 | updateText() 348 | } 349 | 350 | var current: Long = 0 351 | set(value) { 352 | field = value 353 | updateText() 354 | } 355 | 356 | override fun paintComponent(g: Graphics?) { 357 | super.paintComponent(g) 358 | g ?: return 359 | 360 | val globalOrDefaultColorScheme = EditorColorsUtil.getGlobalOrDefaultColorScheme() 361 | 362 | g.color = 363 | if (max > 0) globalOrDefaultColorScheme.getAttributes(CodeInsightColors.LINE_NONE_COVERAGE).foregroundColor 364 | else JBColor.LIGHT_GRAY 365 | val insets = border.getBorderInsets(this) 366 | g.fillRect(insets.left, insets.top, width - insets.right - insets.left, height - insets.bottom - insets.top) 367 | 368 | if (max > 0) { 369 | g.color = globalOrDefaultColorScheme.getAttributes(CodeInsightColors.LINE_FULL_COVERAGE).foregroundColor 370 | g.fillRect( 371 | insets.left, 372 | insets.top, 373 | if (max != 0L) ((width - insets.right - insets.left) * current.toDouble() / max).toInt() else width - insets.right - insets.left, 374 | height - insets.bottom - insets.top 375 | ) 376 | } 377 | 378 | val textWidth = g.fontMetrics.stringWidth(text) 379 | val textHeight = g.fontMetrics.height 380 | g.color = JBColor.BLACK 381 | g.drawString( 382 | text, 383 | insets.left + ((width - insets.right - insets.left) / 2 - textWidth / 2), 384 | insets.bottom + ((height - insets.top - insets.bottom) / 2 + textHeight / 2) 385 | ) 386 | } 387 | } 388 | 389 | private class ProgressBarColumn( 390 | title: String, 391 | private val maxFunc: (DefaultMutableTreeNode) -> Long, 392 | private val currFunc: (DefaultMutableTreeNode) -> Long 393 | ) : ColumnInfo(title) { 394 | override fun valueOf(item: DefaultMutableTreeNode?): Component? { 395 | item ?: return null 396 | return CoverageBar().apply { 397 | max = maxFunc(item) 398 | current = currFunc(item) 399 | } 400 | } 401 | 402 | override fun getRenderer(item: DefaultMutableTreeNode?): TableCellRenderer { 403 | return TableCellRenderer { _, value, _, _, _, _ -> value as CoverageBar } 404 | } 405 | 406 | override fun getColumnClass(): Class<*> { 407 | return DefaultMutableTreeNode::class.java 408 | } 409 | } 410 | 411 | private fun getColumnInfo(hasBranchCoverage: Boolean): Array> { 412 | 413 | val fileInfo = TreeColumnInfo("File/Function") 414 | val branchInfo = ProgressBarColumn("Branch coverage", { treeNode -> 415 | when (val userObject = treeNode.userObject) { 416 | is CoverageFunctionData -> if (userObject.branches.isEmpty()) 0 else 100 417 | is CoverageFileData -> if (userObject.functions.values.all { it.branches.isEmpty() }) 0 else 100 418 | else -> 100 419 | } 420 | }) { 421 | getBranchCoverage(it.userObject) 422 | } 423 | val lineInfo = ProgressBarColumn("Line/Region coverage", { 424 | getMaxLineCoverage(it.userObject) 425 | }) { 426 | getCurrentLineCoverage(it.userObject) 427 | } 428 | 429 | return if (hasBranchCoverage) { 430 | arrayOf(fileInfo, branchInfo, lineInfo) 431 | } else { 432 | arrayOf(fileInfo, lineInfo) 433 | } 434 | } 435 | 436 | private fun getCurrentLineCoverage(functionOrFileData: Any): Long { 437 | fun fromFunctionData(it: CoverageFunctionData): Long { 438 | return when (it.coverage) { 439 | is FunctionLineData -> it.coverage.data.count { entry -> entry.value > 0 }.toLong() 440 | is FunctionRegionData -> it.coverage.data.filter { region -> region.kind != FunctionRegionData.Region.Kind.Gap } 441 | .count { region -> region.executionCount > 0 }.toLong() 442 | } 443 | } 444 | 445 | return when (functionOrFileData) { 446 | is CoverageFunctionData -> fromFunctionData(functionOrFileData) 447 | is CoverageFileData -> { 448 | functionOrFileData.functions.values.sumOf(::fromFunctionData) 449 | } 450 | else -> 0 451 | } 452 | } 453 | 454 | private fun getMaxLineCoverage(functionOrFileData: Any): Long { 455 | fun fromFunctionData(it: CoverageFunctionData): Long { 456 | return when (it.coverage) { 457 | is FunctionLineData -> it.coverage.data.size.toLong() 458 | is FunctionRegionData -> it.coverage.data.count { region -> region.kind != FunctionRegionData.Region.Kind.Gap } 459 | .toLong() 460 | } 461 | } 462 | 463 | return when (functionOrFileData) { 464 | is CoverageFunctionData -> fromFunctionData(functionOrFileData) 465 | is CoverageFileData -> { 466 | functionOrFileData.functions.values.sumOf(::fromFunctionData) 467 | } 468 | else -> 0L 469 | } 470 | } 471 | 472 | private fun getBranchCoverage(functionOrFileData: Any): Long { 473 | fun fromFunctionData(it: CoverageFunctionData): Long? { 474 | if (it.branches.isEmpty()) { 475 | return null 476 | } 477 | return it.branches.map { op -> 478 | when { 479 | op.skippedCount != 0 && op.steppedInCount != 0 -> 100 480 | op.skippedCount != 0 || op.steppedInCount != 0 -> 50 481 | else -> 0 482 | } 483 | }.average().toLong() 484 | } 485 | 486 | return when (functionOrFileData) { 487 | is CoverageFunctionData -> fromFunctionData(functionOrFileData) ?: 0 488 | is CoverageFileData -> { 489 | if (functionOrFileData.functions.isEmpty()) { 490 | 100L 491 | } else { 492 | functionOrFileData.functions.values.mapNotNull(::fromFunctionData).average().toLong() 493 | } 494 | 495 | } 496 | else -> 0 497 | } 498 | } --------------------------------------------------------------------------------