diff --git a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/ProjectConfigurationHandler.kt b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/ProjectConfigurationHandler.kt index b676d3f0..2e7f671f 100644 --- a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/ProjectConfigurationHandler.kt +++ b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/ProjectConfigurationHandler.kt @@ -15,7 +15,7 @@ import org.utbot.cpp.clion.plugin.utils.notifyError import org.utbot.cpp.clion.plugin.utils.notifyInfo import org.utbot.cpp.clion.plugin.utils.notifyUnknownResponse import org.utbot.cpp.clion.plugin.utils.notifyWarning -import org.utbot.cpp.clion.plugin.utils.refreshAndFindNioFile +import org.utbot.cpp.clion.plugin.utils.markDirtyAndRefresh import testsgen.Testgen abstract class ProjectConfigResponseHandler( @@ -98,7 +98,7 @@ class CreateBuildDirHandler( } else -> notifyUnknownResponse(response, project) } - refreshAndFindNioFile(project.settings.buildDirPath) + markDirtyAndRefresh(project.settings.buildDirPath) } } @@ -117,6 +117,6 @@ class GenerateJsonHandler( ) else -> notifyUnknownResponse(response, project) } - refreshAndFindNioFile(project.settings.buildDirPath) + markDirtyAndRefresh(project.settings.buildDirPath) } } diff --git a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/SourceCode.kt b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/SourceCode.kt new file mode 100644 index 00000000..b7c600be --- /dev/null +++ b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/SourceCode.kt @@ -0,0 +1,22 @@ +package org.utbot.cpp.clion.plugin.client.handlers + +import com.intellij.openapi.project.Project +import org.utbot.cpp.clion.plugin.utils.convertFromRemotePathIfNeeded +import testsgen.Util +import java.nio.file.Path + +class SourceCode private constructor( + val localPath: Path, + val remotePath: String, + val content: String, + val regressionMethodsNumber: Int, + val errorMethodsNumber: Int +) { + constructor(serverSourceCode: Util.SourceCode, project: Project) : this( + serverSourceCode.filePath.convertFromRemotePathIfNeeded(project), + serverSourceCode.filePath, + serverSourceCode.code, + serverSourceCode.regressionMethodsNumber, + serverSourceCode.errorMethodsNumber + ) +} diff --git a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/TestsStreamHandler.kt b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/TestsStreamHandler.kt index 411e68ef..f889bb0f 100644 --- a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/TestsStreamHandler.kt +++ b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/handlers/TestsStreamHandler.kt @@ -1,64 +1,123 @@ package org.utbot.cpp.clion.plugin.client.handlers +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import com.intellij.util.io.exists import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +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.isSarifReport import org.utbot.cpp.clion.plugin.utils.logger -import org.utbot.cpp.clion.plugin.utils.refreshAndFindNioFile +import org.utbot.cpp.clion.plugin.utils.markDirtyAndRefresh +import org.utbot.cpp.clion.plugin.utils.nioPath import testsgen.Testgen import testsgen.Util +import java.nio.file.Files import java.nio.file.Path +import java.nio.file.Paths +import java.time.ZoneId class TestsStreamHandler( project: Project, grpcStream: Flow, progressName: String, cancellationJob: Job, - private val onSuccess: (List)->Unit = {}, - private val onError: (Throwable)->Unit = {} -): StreamHandlerWithProgress(project, grpcStream, progressName, cancellationJob) { + private val onSuccess: (List) -> Unit = {}, + private val onError: (Throwable) -> Unit = {} +) : StreamHandlerWithProgress(project, grpcStream, progressName, cancellationJob) { + private val myGeneratedTestFilesLocalFS: MutableList = mutableListOf() override fun onData(data: Testgen.TestsResponse) { super.onData(data) - handleSourceCode(data.testSourcesList) - if (data.hasStubs()) { - handleSourceCode(data.stubs.stubSourcesList, true) - } + + val testSourceCodes = data.testSourcesList + .map { SourceCode(it, project) } + .filter { !it.localPath.isSarifReport() } + handleTestSources(testSourceCodes) + + val stubSourceCodes = data.stubs.stubSourcesList.map { SourceCode(it, project) } + handleStubSources(stubSourceCodes) + + val sarifReport = + data.testSourcesList.find { it.filePath.convertFromRemotePathIfNeeded(project).isSarifReport() }?.let { + SourceCode(it, project) + } + sarifReport?.let { handleSarifReport(it) } + + // for new generated tests remove previous testResults + project.service().clearTestResults(testSourceCodes) + + // tell ide to refresh vfs and refresh project tree + markDirtyAndRefresh(project.nioPath) } - override fun Testgen.TestsResponse.getProgress(): Util.Progress { - return progress + private fun handleSarifReport(sarif: SourceCode) { + backupPreviousClientSarifReport(sarif.localPath) + createSourceCodeFiles(listOf(sarif), "sarif report") + project.logger.info { "Generated SARIF report file ${sarif.localPath}" } } - private fun handleSourceCode(sources: List, isStubs: Boolean = false) { - sources.forEach { sourceCode -> - val filePath: Path = sourceCode.filePath.convertFromRemotePathIfNeeded(project) + private fun handleTestSources(sources: List) { + if (project.settings.isRemoteScenario) { + createSourceCodeFiles(sources, "test") + } - if (!isStubs) - myGeneratedTestFilesLocalFS.add(filePath) + // prepare list of generated test files for further processing + myGeneratedTestFilesLocalFS.addAll(sources.map { it.localPath }) - if (sourceCode.code.isNotEmpty()) { - project.logger.trace { "Creating generated test file: $filePath." } - createFileWithText( - filePath, - sourceCode.code - ) + sources.forEach { sourceCode -> + val isTestSourceFile = sourceCode.localPath.endsWith("_test.cpp") + val testsGenerationResultMessage = if (isTestSourceFile) { + "Generated ${sourceCode.regressionMethodsNumber} tests in regression suite" + + " and ${sourceCode.errorMethodsNumber} tests in error suite" + } else { + // .h file + "Generated test file ${sourceCode.localPath}" } + logger.info(testsGenerationResultMessage) + } + } - var infoMessage = "Generated " + if (isStubs) "stub" else "test" + " file" - if (isGeneratedFileTestSourceFile(filePath.toString())) - infoMessage += " with ${sourceCode.regressionMethodsNumber} tests in regression suite" + - " and ${sourceCode.errorMethodsNumber} tests in error suite" - project.logger.info { "$infoMessage: $filePath" } + private fun handleStubSources(sources: List) { + if (project.settings.isRemoteScenario) { + createSourceCodeFiles(sources, "stub") + } + } - refreshAndFindNioFile(filePath) + private fun createSourceCodeFiles(sourceCodes: List, fileKind: String) { + sourceCodes.forEach { + project.logger.info { "Write $fileKind file ${it.remotePath} to ${it.localPath}" } + createFileWithText(it.localPath, it.content) } } - private fun isGeneratedFileTestSourceFile(fileName: String) = fileName.endsWith("_test.cpp") + override fun Testgen.TestsResponse.getProgress(): Util.Progress = progress + + private fun backupPreviousClientSarifReport(previousReportPaths: Path) { + fun Number.pad2(): String = ("0$this").takeLast(2) + + if (previousReportPaths.exists()) { + val ctime = Files.getLastModifiedTime(previousReportPaths) + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + + val newReportName = "project_code_analysis-" + + ctime.year.toString() + + (ctime.monthValue + 1).pad2() + + ctime.dayOfMonth.pad2() + + ctime.hour.pad2() + + ctime.minute.pad2() + + ctime.second.pad2() + + ".sarif" + val newPath = Paths.get(previousReportPaths.parent.toString(), newReportName) + Files.move(previousReportPaths, newPath) + } + } override fun onCompletion(exception: Throwable?) { super.onCompletion(exception) diff --git a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/requests/test/BaseTestsRequest.kt b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/requests/test/BaseTestsRequest.kt index 4d11e570..8ecde326 100644 --- a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/requests/test/BaseTestsRequest.kt +++ b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/client/requests/test/BaseTestsRequest.kt @@ -35,13 +35,10 @@ abstract class BaseTestsRequest(request: R, project: Project, private val pro } } - open fun getFocusTarget(generatedTestFiles: List): Path? { - return generatedTestFiles.filter { !isHeaderFile(it) && !isSarifReport(it) }.getLongestCommonPathFromRoot() - } - - override fun logRequest() { - logger.info { "$logMessage \n$request" } - } + open fun getFocusTarget(generatedTestFiles: List): Path? = + generatedTestFiles + .filter { !isHeaderFile(it) && !it.isSarifReport() } + .getLongestCommonPathFromRoot() open fun getInfoMessage() = "Tests generated!" diff --git a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/ui/services/TestsResultsStorage.kt b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/ui/services/TestsResultsStorage.kt index aa9fc3dc..e35bebd5 100644 --- a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/ui/services/TestsResultsStorage.kt +++ b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/ui/services/TestsResultsStorage.kt @@ -5,9 +5,7 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.newvfs.BulkFileListener -import com.intellij.openapi.vfs.newvfs.events.VFileEvent +import org.utbot.cpp.clion.plugin.client.handlers.SourceCode import org.utbot.cpp.clion.plugin.listeners.UTBotTestResultsReceivedListener import org.utbot.cpp.clion.plugin.utils.convertFromRemotePathIfNeeded import testsgen.Testgen @@ -31,31 +29,18 @@ class TestsResultsStorage(val project: Project) { forceGutterIconsUpdate() }) - - connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener { - override fun after(events: MutableList) { - var wasSave = false - events.forEach { event -> - if (event.isFromSave) { - wasSave = true - storage.forEach { entry -> - if (entry.value.testFilePath != event.path) { - storage.remove(entry.key) - } - } - } - } - - if (wasSave) { - forceGutterIconsUpdate() - } - } - }) - } fun getTestResultByTestName(testName: String): Testgen.TestResultObject? = storage[testName] + /** + * Cleans the results of previous test run if tests were regenerated. + */ + fun clearTestResults(sourceCodes: List) { + val localFilePaths = sourceCodes.map { it.localPath }.toSet() + storage.values.removeIf { it.testFilePath.convertFromRemotePathIfNeeded(project) in localFilePaths } + } + private fun shouldForceUpdate(): Boolean { val currentlyOpenedFilePaths = FileEditorManager.getInstance(project) .selectedEditors diff --git a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/FileUtils.kt b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/FileUtils.kt index ed4e64ca..58ffb235 100644 --- a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/FileUtils.kt +++ b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/FileUtils.kt @@ -6,9 +6,12 @@ import com.intellij.util.io.exists import kotlin.io.path.writeText import java.nio.file.Path -fun refreshAndFindNioFile(path: Path, async: Boolean = true, recursive: Boolean = true, reloadChildren: Boolean = true) { - VfsUtil.markDirtyAndRefresh(async, recursive, reloadChildren, path.toFile()) -} +fun markDirtyAndRefresh( + path: Path, + async: Boolean = true, + recursive: Boolean = true, + reloadChildren: Boolean = true, +) = VfsUtil.markDirtyAndRefresh(async, recursive, reloadChildren, path.toFile()) fun createFileWithText(filePath: Path, text: String) { with(filePath) { @@ -24,7 +27,3 @@ fun isCPPFileName(fileName: String) = """.*\.(cpp|hpp|h)""".toRegex().matches(fi fun isHeaderFile(fileName: String) = """.*\.([ch])""".toRegex().matches(fileName) fun isHeaderFile(path: Path) = isHeaderFile(path.fileName.toString()) - -fun isSarifReport(fileName: String) = fileName.endsWith(".sarif") - -fun isSarifReport(path: Path) = isSarifReport(path.fileName.toString()) diff --git a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/PathUtils.kt b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/PathUtils.kt index f5ff421f..c469a7ed 100644 --- a/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/PathUtils.kt +++ b/clion-plugin/src/main/kotlin/org/utbot/cpp/clion/plugin/utils/PathUtils.kt @@ -15,6 +15,7 @@ import java.util.* import kotlin.io.path.div val Project.path get() = this.basePath ?: error("Project path can't be null!") +val Project.nioPath: Path get() = Paths.get(this.path) fun relativize(from: String, to: String): String { val toPath = Paths.get(to) @@ -51,6 +52,8 @@ fun Path.visitAllDirectories(action: (Path) -> Unit) { } } +fun Path.isSarifReport() = this.fileName.toString().endsWith(".sarif") + fun String.fileNameOrNull(): String? { return try { Paths.get(this).fileName.toString()