Skip to content

Commit

Permalink
prototype cmake generation
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
vol0n committed Oct 31, 2022
1 parent 6bffa2e commit b41ad79
Show file tree
Hide file tree
Showing 79 changed files with 1,264 additions and 164 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.utbot.cpp.clion.plugin.actions

import com.intellij.openapi.actionSystem.AnActionEvent
import org.utbot.cpp.clion.plugin.utils.client

class SyncWrappersAndStubsAction: UTBotBaseAction() {
override fun actionPerformed(e: AnActionEvent) {
e.client.syncWrappersAnsStubs()
}

override fun updateIfEnabled(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.project != null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import org.utbot.cpp.clion.plugin.actions.ShowSettingsAction
import org.utbot.cpp.clion.plugin.client.channels.LogChannel
import org.utbot.cpp.clion.plugin.grpc.IllegalPathException
import org.utbot.cpp.clion.plugin.client.logger.ClientLogger
import org.utbot.cpp.clion.plugin.client.requests.SyncProjectStubsAndWrappers
import org.utbot.cpp.clion.plugin.grpc.GrpcRequestBuilderFactory
import org.utbot.cpp.clion.plugin.listeners.ConnectionStatus
import org.utbot.cpp.clion.plugin.listeners.UTBotEventsListener
import org.utbot.cpp.clion.plugin.settings.projectIndependentSettings
Expand Down Expand Up @@ -83,46 +85,46 @@ class Client(
)
return
}
executeRequestImpl(request)
requestsCS.launch(CoroutineName(request.toString())) {
executeRequestImpl(request, coroutineContext[Job])
}
}

private fun executeRequestImpl(request: Request) {
requestsCS.launch(CoroutineName(request.toString())) {
try {
request.execute(stub, coroutineContext[Job])
} catch (e: io.grpc.StatusException) {
val id = request.id
when (e.status.code) {
Status.UNAVAILABLE.code -> notifyNotConnected(project, port, serverName)
Status.UNKNOWN.code -> notifyError(
UTBot.message("notify.title.unknown.server.error"), // unknown server error
UTBot.message("notify.unknown.server.error"),
project
)
Status.CANCELLED.code -> notifyError(
UTBot.message("notify.title.cancelled"),
UTBot.message("notify.cancelled", id, e.message ?: ""),
project
)
Status.FAILED_PRECONDITION.code, Status.INTERNAL.code, Status.UNIMPLEMENTED.code, Status.INVALID_ARGUMENT.code -> notifyError(
UTBot.message("notify.title.error"),
UTBot.message("notify.request.failed", e.message ?: "", id),
project
)
else -> notifyError(
UTBot.message("notify.title.error"),
e.message ?: "Corresponding exception's message is missing",
project
)
}
} catch (e: IllegalPathException) {
notifyError(
UTBot.message("notify.bad.settings.title"),
UTBot.message("notify.bad.path", e.message ?: ""),
project,
ShowSettingsAction()
private suspend fun executeRequestImpl(request: Request, job: Job?) {
try {
request.execute(stub, job)
} catch (e: io.grpc.StatusException) {
val id = request.id
when (e.status.code) {
Status.UNAVAILABLE.code -> notifyNotConnected(project, port, serverName)
Status.UNKNOWN.code -> notifyError(
UTBot.message("notify.title.unknown.server.error"), // unknown server error
UTBot.message("notify.unknown.server.error"),
project
)
Status.CANCELLED.code -> notifyError(
UTBot.message("notify.title.cancelled"),
UTBot.message("notify.cancelled", id, e.message ?: ""),
project
)
Status.FAILED_PRECONDITION.code, Status.INTERNAL.code, Status.UNIMPLEMENTED.code, Status.INVALID_ARGUMENT.code -> notifyError(
UTBot.message("notify.title.error"),
UTBot.message("notify.request.failed", e.message ?: "", id),
project
)
else -> notifyError(
UTBot.message("notify.title.error"),
e.message ?: "Corresponding exception's message is missing",
project
)
}
} catch (e: IllegalPathException) {
notifyError(
UTBot.message("notify.bad.settings.title"),
UTBot.message("notify.bad.path", e.message ?: ""),
project,
ShowSettingsAction()
)
}
}

Expand All @@ -134,17 +136,27 @@ class Client(
}
}

private fun registerClient() {
requestsCS.launch {
try {
logger.info { "Sending REGISTER CLIENT request, clientID == $clientId" }
stub.registerClient(Testgen.RegisterClientRequest.newBuilder().setClientId(clientId).build())
} catch (e: io.grpc.StatusException) {
logger.error { "${e.status}: ${e.message}" }
}
private suspend fun registerClient() {
try {
logger.info { "Sending REGISTER CLIENT request, clientID == $clientId" }
stub.registerClient(Testgen.RegisterClientRequest.newBuilder().setClientId(clientId).build())
} catch (e: io.grpc.StatusException) {
logger.error { "Exception on registering client: ${e.status}: ${e.message}" }
}
}

fun syncWrappersAndStubs() {
createRequestForSync().also {
executeRequestIfNotDisposed(it)
}
}

private fun createRequestForSync(): SyncProjectStubsAndWrappers =
SyncProjectStubsAndWrappers(
GrpcRequestBuilderFactory(project).createProjectRequestBuilder(),
project
)

private fun startPeriodicHeartBeat() {
logger.info { "The heartbeat started with interval: $HEARTBEAT_INTERVAL ms" }
servicesCS.launch(CoroutineName("periodicHeartBeat")) {
Expand All @@ -167,6 +179,7 @@ class Client(
notifyInfo(UTBot.message("notify.connected.title"), UTBot.message("notify.connected", port, serverName))
logger.info { "Successfully connected to server!" }
registerClient()
executeRequestIfNotDisposed(createRequestForSync())
}

if (newClient || !response.linked) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ class ManagedClient(val project: Project) : Disposable {
return "${(System.getenv("USER") ?: "user")}-${createRandomSequence()}"
}

fun syncWrappersAnsStubs() {
client?.syncWrappersAndStubs()
}

@TestOnly
fun waitForServerRequestsToFinish(
timeout: Long = SERVER_TIMEOUT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.utbot.cpp.clion.plugin.client.handlers.testsStreamHandler

class CMakePrinter(private val currentCMakeListsContent: String) {
private val ss = StringBuilder()
var isEmpty = true
private set

init {
ss.append("\n")
startUTBotSection()
}

private fun add(string: String) {
ss.append(string)
ss.append("\n")
}

fun startUTBotSection() {
add("#utbot_section_start")
}

fun addDownloadGTestSection() {
isEmpty = false
add(
"""
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
include(GoogleTest)
""".trimIndent()
)
}

fun addSubdirectory(dirRelativePath: String) {
isEmpty = false
val addDirectoryInstruction = "add_subdirectory($dirRelativePath)"
if (!currentCMakeListsContent.contains(addDirectoryInstruction))
add(addDirectoryInstruction)
}

fun get(): String {
return ss.toString() + "#utbot_section_end\n"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.utbot.cpp.clion.plugin.client.handlers.testsStreamHandler

import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.dsl.builder.panel
import javax.swing.JComponent
import org.utbot.cpp.clion.plugin.UTBot

class ShouldInstallGTestDialog(project: Project) : DialogWrapper(project) {
init {
init()
title = "UTBot: GTest Install"
}

override fun createCenterPanel(): JComponent {
return panel {
row(UTBot.message("dialog.should.install.gtest")) {}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
package org.utbot.cpp.clion.plugin.client.handlers
package org.utbot.cpp.clion.plugin.client.handlers.testsStreamHandler

import com.intellij.openapi.components.service
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.util.io.exists
import com.intellij.util.io.readText
import kotlin.io.path.appendText
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import org.utbot.cpp.clion.plugin.UTBot
import org.utbot.cpp.clion.plugin.client.handlers.SourceCode
import org.utbot.cpp.clion.plugin.client.handlers.StreamHandlerWithProgress
import org.utbot.cpp.clion.plugin.settings.settings
import org.utbot.cpp.clion.plugin.ui.services.TestsResultsStorage
import org.utbot.cpp.clion.plugin.utils.convertFromRemotePathIfNeeded
import org.utbot.cpp.clion.plugin.utils.createFileWithText
import org.utbot.cpp.clion.plugin.utils.invokeOnEdt
import org.utbot.cpp.clion.plugin.utils.isCMakeListsFile
import org.utbot.cpp.clion.plugin.utils.isSarifReport
import org.utbot.cpp.clion.plugin.utils.logger
import org.utbot.cpp.clion.plugin.utils.markDirtyAndRefresh
import org.utbot.cpp.clion.plugin.utils.nioPath
import org.utbot.cpp.clion.plugin.utils.notifyError
import testsgen.Testgen
import testsgen.Util
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
Expand All @@ -31,34 +41,91 @@ class TestsStreamHandler(
) : StreamHandlerWithProgress<Testgen.TestsResponse>(project, grpcStream, progressName, cancellationJob) {

private val myGeneratedTestFilesLocalFS: MutableList<Path> = mutableListOf()
private var isCMakePresent = false
private var isSarifPresent = false

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

val testSourceCodes = data.testSourcesList
.map { SourceCode(it, project) }
.filter { !it.localPath.isSarifReport() }
// currently testSourcesList contains not only test sourse codes but
// also some extra files like sarif report, cmake generated file
// this was done for compatibility
val sourceCodes = data.testSourcesList.mapNotNull { it.toSourceCodeOrNull() }
val testSourceCodes = sourceCodes
.filter { !it.localPath.isSarifReport() && !it.localPath.isCMakeListsFile() }
handleTestSources(testSourceCodes)

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

val sarifReport =
data.testSourcesList.find { it.filePath.convertFromRemotePathIfNeeded(project).isSarifReport() }?.let {
SourceCode(it, project)
}
sarifReport?.let { handleSarifReport(it) }
val sarifReport = sourceCodes.find { it.localPath.isSarifReport() }
if (sarifReport != null)
handleSarifReport(sarifReport)

val cmakeFile = sourceCodes.find { it.localPath.endsWith("CMakeLists.txt") }
if (cmakeFile != null)
handleCMakeFile(cmakeFile)

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

override fun onFinish() {
super.onFinish()
if (!isCMakePresent)
project.logger.warn("CMake file is missing in the tests response")
if (!isSarifPresent)
project.logger.warn("Sarif report is missing in the tests response")
// tell ide to refresh vfs and refresh project tree
markDirtyAndRefresh(project.nioPath)
}

private fun handleCMakeFile(cmakeSourceCode: SourceCode) {
isCMakePresent = true
createFileWithText(cmakeSourceCode.localPath, cmakeSourceCode.content)
val rootCMakeFile = project.nioPath.resolve("CMakeLists.txt")
if (!rootCMakeFile.exists()) {
project.logger.warn("Root CMakeLists.txt file does not exist. Skipping CMake patches.")
return
}

val currentCMakeFileContent = rootCMakeFile.readText()
val cMakePrinter = CMakePrinter(currentCMakeFileContent)
invokeOnEdt { // we can show dialog only from edt

if (!project.settings.storedSettings.isGTestInstalled) {
val shouldInstallGTestDialog = ShouldInstallGTestDialog(project)

if (shouldInstallGTestDialog.showAndGet()) {
cMakePrinter.addDownloadGTestSection()
}

// whether user confirmed that gtest is installed or we added the gtest section, from now on
// we will assume that gtest is installed
project.settings.storedSettings.isGTestInstalled = true
}

cMakePrinter.addSubdirectory(project.settings.storedSettings.testDirRelativePath)

// currently we are on EDT, but writing to file better to be done on background thread
ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Modifying CMakeLists.txt file", false) {
override fun run(progressIndicator: ProgressIndicator) {
try {
if (!cMakePrinter.isEmpty)
project.nioPath.resolve("CMakeLists.txt").appendText(cMakePrinter.get())
} catch (e: IOException) {
notifyError(
UTBot.message("notify.title.error"),
UTBot.message("notify.error.write.to.file", e.message ?: "unknown reason"),
project
)
}
}
})
}
}

override fun onCompletion(exception: Throwable?) {
invokeOnEdt {
indicator.stopShowingProgressInUI()
Expand All @@ -71,6 +138,7 @@ class TestsStreamHandler(
}

private fun handleSarifReport(sarif: SourceCode) {
isSarifPresent = true
backupPreviousClientSarifReport(sarif.localPath)
createSourceCodeFiles(listOf(sarif), "sarif report")
project.logger.info { "Generated SARIF report file ${sarif.localPath}" }
Expand All @@ -97,6 +165,15 @@ class TestsStreamHandler(
}
}

private fun Util.SourceCode.toSourceCodeOrNull(): SourceCode? {
return try {
SourceCode(this, project)
} catch (e: IllegalArgumentException) {
project.logger.error("Could not convert remote path to local version: bad path: ${this.filePath}")
null
}
}

private fun handleStubSources(sources: List<SourceCode>) {
if (project.settings.isRemoteScenario) {
createSourceCodeFiles(sources, "stub")
Expand Down
Loading

0 comments on commit b41ad79

Please sign in to comment.