├── .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 |
4 |
5 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
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 |
--------------------------------------------------------------------------------
/src/main/java/net/zero9178/cov/window/CoverageView.form:
--------------------------------------------------------------------------------
1 |
2 |
49 |
--------------------------------------------------------------------------------
/.idea/libraries-with-intellij-classes.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 |
--------------------------------------------------------------------------------
/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 |
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 |
104 |
--------------------------------------------------------------------------------
/src/main/resources/icons/CoverageRunAction_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
160 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/pluginIcon_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
198 |
--------------------------------------------------------------------------------
/src/main/java/net/zero9178/cov/window/SettingsWindow.form:
--------------------------------------------------------------------------------
1 |
2 |
180 |
--------------------------------------------------------------------------------
/src/main/resources/icons/TemplateLineMarker_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 | }
--------------------------------------------------------------------------------