Skip to content

Commit f99e227

Browse files
committed
prototype cmake generation
- CMake file is generated, it assembles tests similar to makefiles generated by UTBOT but simpler - Wrappers are sent to client in the same way as stubs - CMake file is adopted in CLion: the tests subdir is added to the root CMakeLists.txt and GTest is optionally installed.
1 parent 6bffa2e commit f99e227

File tree

75 files changed

+1238
-153
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+1238
-153
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.utbot.cpp.clion.plugin.actions
2+
3+
import com.intellij.openapi.actionSystem.AnActionEvent
4+
import org.utbot.cpp.clion.plugin.utils.client
5+
6+
class SyncWrappersAndStubsAction: UTBotBaseAction() {
7+
override fun actionPerformed(e: AnActionEvent) {
8+
e.client.syncWrappersAnsStubs()
9+
}
10+
11+
override fun updateIfEnabled(e: AnActionEvent) {
12+
e.presentation.isEnabledAndVisible = e.project != null
13+
}
14+
}

clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/Client.kt

+57-44
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import org.utbot.cpp.clion.plugin.actions.ShowSettingsAction
2222
import org.utbot.cpp.clion.plugin.client.channels.LogChannel
2323
import org.utbot.cpp.clion.plugin.grpc.IllegalPathException
2424
import org.utbot.cpp.clion.plugin.client.logger.ClientLogger
25+
import org.utbot.cpp.clion.plugin.client.requests.SyncProjectStubsAndWrappers
26+
import org.utbot.cpp.clion.plugin.grpc.GrpcRequestBuilderFactory
2527
import org.utbot.cpp.clion.plugin.listeners.ConnectionStatus
2628
import org.utbot.cpp.clion.plugin.listeners.UTBotEventsListener
2729
import org.utbot.cpp.clion.plugin.settings.projectIndependentSettings
@@ -83,46 +85,46 @@ class Client(
8385
)
8486
return
8587
}
86-
executeRequestImpl(request)
88+
requestsCS.launch(CoroutineName(request.toString())) {
89+
executeRequestImpl(request, coroutineContext[Job])
90+
}
8791
}
8892

89-
private fun executeRequestImpl(request: Request) {
90-
requestsCS.launch(CoroutineName(request.toString())) {
91-
try {
92-
request.execute(stub, coroutineContext[Job])
93-
} catch (e: io.grpc.StatusException) {
94-
val id = request.id
95-
when (e.status.code) {
96-
Status.UNAVAILABLE.code -> notifyNotConnected(project, port, serverName)
97-
Status.UNKNOWN.code -> notifyError(
98-
UTBot.message("notify.title.unknown.server.error"), // unknown server error
99-
UTBot.message("notify.unknown.server.error"),
100-
project
101-
)
102-
Status.CANCELLED.code -> notifyError(
103-
UTBot.message("notify.title.cancelled"),
104-
UTBot.message("notify.cancelled", id, e.message ?: ""),
105-
project
106-
)
107-
Status.FAILED_PRECONDITION.code, Status.INTERNAL.code, Status.UNIMPLEMENTED.code, Status.INVALID_ARGUMENT.code -> notifyError(
108-
UTBot.message("notify.title.error"),
109-
UTBot.message("notify.request.failed", e.message ?: "", id),
110-
project
111-
)
112-
else -> notifyError(
113-
UTBot.message("notify.title.error"),
114-
e.message ?: "Corresponding exception's message is missing",
115-
project
116-
)
117-
}
118-
} catch (e: IllegalPathException) {
119-
notifyError(
120-
UTBot.message("notify.bad.settings.title"),
121-
UTBot.message("notify.bad.path", e.message ?: ""),
122-
project,
123-
ShowSettingsAction()
93+
private suspend fun executeRequestImpl(request: Request, job: Job?) {
94+
try {
95+
request.execute(stub, job)
96+
} catch (e: io.grpc.StatusException) {
97+
val id = request.id
98+
when (e.status.code) {
99+
Status.UNAVAILABLE.code -> notifyNotConnected(project, port, serverName)
100+
Status.UNKNOWN.code -> notifyError(
101+
UTBot.message("notify.title.unknown.server.error"), // unknown server error
102+
UTBot.message("notify.unknown.server.error"),
103+
project
104+
)
105+
Status.CANCELLED.code -> notifyError(
106+
UTBot.message("notify.title.cancelled"),
107+
UTBot.message("notify.cancelled", id, e.message ?: ""),
108+
project
109+
)
110+
Status.FAILED_PRECONDITION.code, Status.INTERNAL.code, Status.UNIMPLEMENTED.code, Status.INVALID_ARGUMENT.code -> notifyError(
111+
UTBot.message("notify.title.error"),
112+
UTBot.message("notify.request.failed", e.message ?: "", id),
113+
project
114+
)
115+
else -> notifyError(
116+
UTBot.message("notify.title.error"),
117+
e.message ?: "Corresponding exception's message is missing",
118+
project
124119
)
125120
}
121+
} catch (e: IllegalPathException) {
122+
notifyError(
123+
UTBot.message("notify.bad.settings.title"),
124+
UTBot.message("notify.bad.path", e.message ?: ""),
125+
project,
126+
ShowSettingsAction()
127+
)
126128
}
127129
}
128130

@@ -134,17 +136,27 @@ class Client(
134136
}
135137
}
136138

137-
private fun registerClient() {
138-
requestsCS.launch {
139-
try {
140-
logger.info { "Sending REGISTER CLIENT request, clientID == $clientId" }
141-
stub.registerClient(Testgen.RegisterClientRequest.newBuilder().setClientId(clientId).build())
142-
} catch (e: io.grpc.StatusException) {
143-
logger.error { "${e.status}: ${e.message}" }
144-
}
139+
private suspend fun registerClient() {
140+
try {
141+
logger.info { "Sending REGISTER CLIENT request, clientID == $clientId" }
142+
stub.registerClient(Testgen.RegisterClientRequest.newBuilder().setClientId(clientId).build())
143+
} catch (e: io.grpc.StatusException) {
144+
logger.error { "Exception on registering client: ${e.status}: ${e.message}" }
145145
}
146146
}
147147

148+
fun syncWrappersAndStubs() {
149+
createRequestForSync().also {
150+
executeRequestIfNotDisposed(it)
151+
}
152+
}
153+
154+
private fun createRequestForSync(): SyncProjectStubsAndWrappers =
155+
SyncProjectStubsAndWrappers(
156+
GrpcRequestBuilderFactory(project).createProjectRequestBuilder(),
157+
project
158+
)
159+
148160
private fun startPeriodicHeartBeat() {
149161
logger.info { "The heartbeat started with interval: $HEARTBEAT_INTERVAL ms" }
150162
servicesCS.launch(CoroutineName("periodicHeartBeat")) {
@@ -167,6 +179,7 @@ class Client(
167179
notifyInfo(UTBot.message("notify.connected.title"), UTBot.message("notify.connected", port, serverName))
168180
logger.info { "Successfully connected to server!" }
169181
registerClient()
182+
executeRequestIfNotDisposed(createRequestForSync())
170183
}
171184

172185
if (newClient || !response.linked) {

clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/ManagedClient.kt

+4
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ class ManagedClient(val project: Project) : Disposable {
9999
return "${(System.getenv("USER") ?: "user")}-${createRandomSequence()}"
100100
}
101101

102+
fun syncWrappersAnsStubs() {
103+
client?.syncWrappersAndStubs()
104+
}
105+
102106
@TestOnly
103107
fun waitForServerRequestsToFinish(
104108
timeout: Long = SERVER_TIMEOUT,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.utbot.cpp.clion.plugin.client.handlers.testsStreamHandler
2+
3+
class CMakePrinter(private val currentCMakeListsContent: String) {
4+
private val ss = StringBuilder()
5+
var isEmpty = true
6+
private set
7+
8+
init {
9+
ss.append("\n")
10+
startUTBotSection()
11+
}
12+
13+
private fun add(string: String) {
14+
ss.append(string)
15+
ss.append("\n")
16+
}
17+
18+
fun startUTBotSection() {
19+
add("#utbot_section_start")
20+
}
21+
22+
fun addDownloadGTestSection() {
23+
isEmpty = false
24+
add(
25+
"""
26+
include(FetchContent)
27+
FetchContent_Declare(
28+
googletest
29+
GIT_REPOSITORY https://github.com/google/googletest.git
30+
GIT_TAG release-1.12.1
31+
)
32+
# For Windows: Prevent overriding the parent project's compiler/linker settings
33+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
34+
FetchContent_MakeAvailable(googletest)
35+
enable_testing()
36+
include(GoogleTest)
37+
""".trimIndent()
38+
)
39+
}
40+
41+
fun addSubdirectory(dirRelativePath: String) {
42+
isEmpty = false
43+
val addDirectoryInstruction = "add_subdirectory($dirRelativePath)"
44+
if (!currentCMakeListsContent.contains(addDirectoryInstruction))
45+
add(addDirectoryInstruction)
46+
}
47+
48+
fun get(): String {
49+
return ss.toString() + "#utbot_section_end\n"
50+
}
51+
}
52+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.utbot.cpp.clion.plugin.client.handlers.testsStreamHandler
2+
3+
import com.intellij.openapi.project.Project
4+
import com.intellij.openapi.ui.DialogWrapper
5+
import com.intellij.ui.dsl.builder.panel
6+
import javax.swing.JComponent
7+
import org.utbot.cpp.clion.plugin.UTBot
8+
9+
class ShouldInstallGTestDialog(project: Project) : DialogWrapper(project) {
10+
init {
11+
init()
12+
title = "UTBot: GTest Install"
13+
}
14+
15+
override fun createCenterPanel(): JComponent {
16+
return panel {
17+
row(UTBot.message("dialog.should.install.gtest")) {}
18+
}
19+
}
20+
}

clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/TestsStreamHandler.kt renamed to clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/testsStreamHandler/TestsStreamHandler.kt

+88-11
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
1-
package org.utbot.cpp.clion.plugin.client.handlers
1+
package org.utbot.cpp.clion.plugin.client.handlers.testsStreamHandler
22

33
import com.intellij.openapi.components.service
4+
import com.intellij.openapi.progress.ProgressIndicator
5+
import com.intellij.openapi.progress.ProgressManager
6+
import com.intellij.openapi.progress.Task
47
import com.intellij.openapi.project.Project
58
import com.intellij.util.io.exists
9+
import com.intellij.util.io.readText
10+
import kotlin.io.path.appendText
611
import kotlinx.coroutines.CancellationException
712
import kotlinx.coroutines.Job
813
import kotlinx.coroutines.flow.Flow
14+
import org.utbot.cpp.clion.plugin.UTBot
15+
import org.utbot.cpp.clion.plugin.client.handlers.SourceCode
16+
import org.utbot.cpp.clion.plugin.client.handlers.StreamHandlerWithProgress
917
import org.utbot.cpp.clion.plugin.settings.settings
1018
import org.utbot.cpp.clion.plugin.ui.services.TestsResultsStorage
11-
import org.utbot.cpp.clion.plugin.utils.convertFromRemotePathIfNeeded
1219
import org.utbot.cpp.clion.plugin.utils.createFileWithText
1320
import org.utbot.cpp.clion.plugin.utils.invokeOnEdt
21+
import org.utbot.cpp.clion.plugin.utils.isCMakeListsFile
1422
import org.utbot.cpp.clion.plugin.utils.isSarifReport
1523
import org.utbot.cpp.clion.plugin.utils.logger
1624
import org.utbot.cpp.clion.plugin.utils.markDirtyAndRefresh
1725
import org.utbot.cpp.clion.plugin.utils.nioPath
26+
import org.utbot.cpp.clion.plugin.utils.notifyError
1827
import testsgen.Testgen
1928
import testsgen.Util
29+
import java.io.IOException
2030
import java.nio.file.Files
2131
import java.nio.file.Path
2232
import java.nio.file.Paths
@@ -31,34 +41,91 @@ class TestsStreamHandler(
3141
) : StreamHandlerWithProgress<Testgen.TestsResponse>(project, grpcStream, progressName, cancellationJob) {
3242

3343
private val myGeneratedTestFilesLocalFS: MutableList<Path> = mutableListOf()
44+
private var isCMakePresent = false
45+
private var isSarifPresent = false
3446

3547
override fun onData(data: Testgen.TestsResponse) {
3648
super.onData(data)
3749

38-
val testSourceCodes = data.testSourcesList
39-
.map { SourceCode(it, project) }
40-
.filter { !it.localPath.isSarifReport() }
50+
// currently testSourcesList contains not only test sourse codes but
51+
// also some extra files like sarif report, cmake generated file
52+
// this was done for compatibility
53+
val sourceCodes = data.testSourcesList.mapNotNull { it.toSourceCodeOrNull() }
54+
val testSourceCodes = sourceCodes
55+
.filter { !it.localPath.isSarifReport() && !it.localPath.isCMakeListsFile() }
4156
handleTestSources(testSourceCodes)
4257

43-
val stubSourceCodes = data.stubs.stubSourcesList.map { SourceCode(it, project) }
58+
// for stubs we know that stubSourcesList contains only stub sources
59+
val stubSourceCodes = data.stubs.stubSourcesList.mapNotNull { it.toSourceCodeOrNull() }
4460
handleStubSources(stubSourceCodes)
4561

46-
val sarifReport =
47-
data.testSourcesList.find { it.filePath.convertFromRemotePathIfNeeded(project).isSarifReport() }?.let {
48-
SourceCode(it, project)
49-
}
50-
sarifReport?.let { handleSarifReport(it) }
62+
val sarifReport = sourceCodes.find { it.localPath.isSarifReport() }
63+
if (sarifReport != null)
64+
handleSarifReport(sarifReport)
65+
66+
val cmakeFile = sourceCodes.find { it.localPath.endsWith("CMakeLists.txt") }
67+
if (cmakeFile != null)
68+
handleCMakeFile(cmakeFile)
5169

5270
// for new generated tests remove previous testResults
5371
project.service<TestsResultsStorage>().clearTestResults(testSourceCodes)
5472
}
5573

5674
override fun onFinish() {
5775
super.onFinish()
76+
if (!isCMakePresent)
77+
project.logger.warn("CMake file is missing in the tests response")
78+
if (!isSarifPresent)
79+
project.logger.warn("Sarif report is missing in the tests response")
5880
// tell ide to refresh vfs and refresh project tree
5981
markDirtyAndRefresh(project.nioPath)
6082
}
6183

84+
private fun handleCMakeFile(cmakeSourceCode: SourceCode) {
85+
isCMakePresent = true
86+
createFileWithText(cmakeSourceCode.localPath, cmakeSourceCode.content)
87+
val rootCMakeFile = project.nioPath.resolve("CMakeLists.txt")
88+
if (!rootCMakeFile.exists()) {
89+
project.logger.warn("Root CMakeLists.txt file does not exist. Skipping CMake patches.")
90+
return
91+
}
92+
93+
val currentCMakeFileContent = rootCMakeFile.readText()
94+
val cMakePrinter = CMakePrinter(currentCMakeFileContent)
95+
invokeOnEdt { // we can show dialog only from edt
96+
97+
if (!project.settings.storedSettings.isGTestInstalled) {
98+
val shouldInstallGTestDialog = ShouldInstallGTestDialog(project)
99+
100+
if (shouldInstallGTestDialog.showAndGet()) {
101+
cMakePrinter.addDownloadGTestSection()
102+
}
103+
104+
// whether user confirmed that gtest is installed or we added the gtest section, from now on
105+
// we will assume that gtest is installed
106+
project.settings.storedSettings.isGTestInstalled = true
107+
}
108+
109+
cMakePrinter.addSubdirectory(project.settings.storedSettings.testDirRelativePath)
110+
111+
// currently we are on EDT, but writing to file better to be done on background thread
112+
ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Modifying CMakeLists.txt file", false) {
113+
override fun run(progressIndicator: ProgressIndicator) {
114+
try {
115+
if (!cMakePrinter.isEmpty)
116+
project.nioPath.resolve("CMakeLists.txt").appendText(cMakePrinter.get())
117+
} catch (e: IOException) {
118+
notifyError(
119+
UTBot.message("notify.title.error"),
120+
UTBot.message("notify.error.write.to.file", e.message ?: "unknown reason"),
121+
project
122+
)
123+
}
124+
}
125+
})
126+
}
127+
}
128+
62129
override fun onCompletion(exception: Throwable?) {
63130
invokeOnEdt {
64131
indicator.stopShowingProgressInUI()
@@ -71,6 +138,7 @@ class TestsStreamHandler(
71138
}
72139

73140
private fun handleSarifReport(sarif: SourceCode) {
141+
isSarifPresent = true
74142
backupPreviousClientSarifReport(sarif.localPath)
75143
createSourceCodeFiles(listOf(sarif), "sarif report")
76144
project.logger.info { "Generated SARIF report file ${sarif.localPath}" }
@@ -97,6 +165,15 @@ class TestsStreamHandler(
97165
}
98166
}
99167

168+
private fun Util.SourceCode.toSourceCodeOrNull(): SourceCode? {
169+
return try {
170+
SourceCode(this, project)
171+
} catch (e: IllegalArgumentException) {
172+
project.logger.error("Could not convert remote path to local version: bad path: ${this.filePath}")
173+
null
174+
}
175+
}
176+
100177
private fun handleStubSources(sources: List<SourceCode>) {
101178
if (project.settings.isRemoteScenario) {
102179
createSourceCodeFiles(sources, "stub")

0 commit comments

Comments
 (0)