Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prototype cmake generation #521

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -103,6 +103,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