diff --git a/utbot-intellij-js/build.gradle.kts b/utbot-intellij-js/build.gradle.kts index bafc50275c..9af87e9c69 100644 --- a/utbot-intellij-js/build.gradle.kts +++ b/utbot-intellij-js/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") implementation(project(":utbot-ui-commons")) + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) //Family implementation(project(":utbot-js")) diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt index cb78711a66..277c95e1aa 100644 --- a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt @@ -2,6 +2,7 @@ package org.utbot.intellij.plugin.language.js import api.JsTestGenerator import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreterManager import com.intellij.lang.ecmascript6.psi.ES6Class import com.intellij.lang.javascript.psi.JSFile import com.intellij.lang.javascript.refactoring.util.JSMemberInfo @@ -12,10 +13,13 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFileFactory import com.intellij.psi.impl.file.PsiDirectoryFactory import com.intellij.util.concurrency.AppExecutorUtil +import framework.codegen.Mocha +import mu.KotlinLogging import org.jetbrains.kotlin.idea.util.application.invokeLater import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.idea.util.application.runWriteAction @@ -26,6 +30,9 @@ import settings.JsDynamicSettings import settings.JsExportsSettings.endComment import settings.JsExportsSettings.startComment import settings.JsTestGenerationSettings.dummyClassName +import utils.JsCmdExec + +private val logger = KotlinLogging.logger {} object JsDialogProcessor { @@ -38,28 +45,37 @@ object JsDialogProcessor { editor: Editor, file: JSFile ) { - createDialog(project, srcModule, fileMethods, focusedMethod, containingFilePath, file)?.let { dialogProcessor -> - if (!dialogProcessor.showAndGet()) return - /* - Since Tern.js accesses containing file, sync with file system required before test generation. - */ - runWriteAction { - with(FileDocumentManager.getInstance()) { - saveDocument(editor.document) + val model = createJsTestModel(project, srcModule, fileMethods, focusedMethod, containingFilePath, file) + (object : Task.Backgroundable( + project, + "Check the requirements" + ) { + override fun run(indicator: ProgressIndicator) { + invokeLater { + getFrameworkLibraryPath(Mocha.displayName.lowercase(), model) + createDialog(model)?.let { dialogProcessor -> + if (!dialogProcessor.showAndGet()) return@invokeLater + // Since Tern.js accesses containing file, sync with file system required before test generation. + runWriteAction { + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + createTests(dialogProcessor.model, containingFilePath, editor) + } } } - createTests(dialogProcessor.model, containingFilePath, editor) - } + }).queue() } - private fun createDialog( + private fun createJsTestModel( project: Project, srcModule: Module, fileMethods: Set, focusedMethod: JSMemberInfo?, filePath: String, file: JSFile - ): JsDialogWindow? { + ): JsTestsModel? { val testModules = srcModule.testModules(project) if (testModules.isEmpty()) { @@ -70,19 +86,42 @@ object JsDialogProcessor { showErrorDialogLater(project, errorMessage, "Test source roots not found") return null } + return JsTestsModel( + project = project, + srcModule = srcModule, + potentialTestModules = testModules, + fileMethods = fileMethods, + selectedMethods = if (focusedMethod != null) setOf(focusedMethod) else emptySet(), + file = file + ).apply { + containingFilePath = filePath + } - return JsDialogWindow( - JsTestsModel( - project = project, - srcModule = srcModule, - potentialTestModules = testModules, - fileMethods = fileMethods, - selectedMethods = if (focusedMethod != null) setOf(focusedMethod) else emptySet(), - file = file - ).apply { - containingFilePath = filePath - } - ) + } + + private fun createDialog( + jsTestsModel: JsTestsModel? + ): JsDialogWindow? { + try { + jsTestsModel?.pathToNode = NodeJsLocalInterpreterManager.getInstance() + .interpreters.first().interpreterSystemIndependentPath + val (_, error) = JsCmdExec.runCommand( + shouldWait = true, + cmd = arrayOf("node", "-v") + ) + if (error.readText().isNotEmpty()) throw NoSuchElementException() + } catch (e: NoSuchElementException) { + Messages.showErrorDialog( + "Node.js interpreter is not found in IDEA settings.\n" + + "Please set it in Settings > Languages & Frameworks > Node.js", + "Requirement Error" + ) + logger.error { "Node.js interpreter was not found in IDEA settings." } + return null + } + return jsTestsModel?.let { + JsDialogWindow(it) + } } private fun unblockDocument(project: Project, document: Document) { @@ -207,3 +246,34 @@ object JsDialogProcessor { } } } + +// TODO(MINOR): Add indicator.text for each installation +fun installMissingRequirement(project: Project, pathToNPM: String, requirement: String) { + val message = """ + Requirement is not installed: + $requirement + Install it? + """.trimIndent() + val result = Messages.showOkCancelDialog( + project, + message, + "Requirement Missmatch Error", + "Install", + "Cancel", + null + ) + + if (result == Messages.CANCEL) + return + + val (_, errorStream) = installRequirement(pathToNPM, requirement, project.basePath) + + val errorText = errorStream.readText() + if (errorText.isNotEmpty()) { + showErrorDialogLater( + project, + "Requirements installing failed with some reason:\n${errorText}", + "Requirements error" + ) + } +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt index fcaef2630f..c2a8a79671 100644 --- a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt @@ -1,6 +1,5 @@ package org.utbot.intellij.plugin.language.js -import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreterManager import com.intellij.lang.javascript.refactoring.ui.JSMemberSelectionTable import com.intellij.lang.javascript.refactoring.util.JSMemberInfo import com.intellij.openapi.ui.ComboBox @@ -37,18 +36,11 @@ class JsDialogWindow(val model: JsTestsModel) : DialogWrapper(model.project) { this.preferredScrollableViewportSize = JBUI.size(-1, height) } - private val nodeInterp = try { - NodeJsLocalInterpreterManager.getInstance().interpreters.first() - } catch (e: NoSuchElementException) { - throw IllegalStateException("Node.js interpreter is not set in the IDEA settings!") - } - private val testSourceFolderField = TestSourceDirectoryChooser(model, model.file.virtualFile) private val testFrameworks = ComboBox(DefaultComboBoxModel(arrayOf(Mocha))) private val nycSourceFileChooserField = NycSourceFileChooser(model) private val coverageMode = CoverageModeButtons -// private var initTestFrameworkPresenceThread: Thread private lateinit var panel: DialogPanel private val timeoutSpinner = @@ -60,16 +52,8 @@ class JsDialogWindow(val model: JsTestsModel) : DialogWrapper(model.project) { ) init { - model.pathToNode = nodeInterp.interpreterSystemDependentPath.replace("\\", "/") model.pathToNPM = model.pathToNode.substringBeforeLast("/") + "/" + "npm" - //TODO: Find out how to find pathToNode from IDEA settings without extra actions from the user - model.pathToNode = "node" title = "Generate Tests with UtBot" -// initTestFrameworkPresenceThread = thread(start = true) { -// JsCgLanguageAssistant.getLanguageTestFrameworkManager().testFrameworks.forEach { -// it.isInstalled = findFrameworkLibrary(it.displayName.lowercase(Locale.getDefault()), model) -// } -// } isResizable = false init() } diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/Utils.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/Utils.kt index 256a040629..17cbc0852b 100644 --- a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/Utils.kt +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/Utils.kt @@ -3,31 +3,20 @@ package org.utbot.intellij.plugin.language.js import com.intellij.openapi.ui.Messages import utils.JsCmdExec import utils.OsProvider +import java.io.BufferedReader -fun getFrameworkLibraryPath(npmPackageName: String, model: JsTestsModel): String? { +fun getFrameworkLibraryPath(npmPackageName: String, model: JsTestsModel?): String? { val (bufferedReader, errorReader) = JsCmdExec.runCommand( - dir = model.project.basePath!!, + dir = model?.project?.basePath!!, shouldWait = true, timeout = 10, - cmd = arrayOf(OsProvider.getProviderByOs().getAbstractivePathTool(), model.pathToNYC) + cmd = arrayOf(OsProvider.getProviderByOs().getAbstractivePathTool(), npmPackageName) ) val input = bufferedReader.readText() val error = errorReader.readText() - if (error.isNotEmpty() or !input.contains(npmPackageName)) { - if (findFrameworkLibrary(npmPackageName, model)) { - Messages.showErrorDialog( - model.project, - "The following packages were not found, please set it in menu manually:\n $npmPackageName", - "$npmPackageName missing!", - ) - } else { - Messages.showErrorDialog( - model.project, - "The following packages are not installed: $npmPackageName \nPlease install it via npm `> npm i -g $npmPackageName`", - "$npmPackageName missing!", - ) - } + if ((error.isNotEmpty() or !input.contains(npmPackageName)) && !findFrameworkLibrary(npmPackageName, model)) { + installMissingRequirement(model.project, model.pathToNPM, npmPackageName) return null } return input.substringBefore(npmPackageName) + npmPackageName @@ -52,3 +41,15 @@ fun findFrameworkLibrary(npmPackageName: String, model: JsTestsModel): Boolean { } return checkForPackageText.contains(npmPackageName) } + +fun installRequirement(pathToNPM: String, requirement: String, installingDir: String?): Pair { + val installationType = if (requirement == "mocha") "-l" else "-g" + + val (buf1, buf2) = JsCmdExec.runCommand( + dir = installingDir, + shouldWait = true, + timeout = 10, + cmd = arrayOf(pathToNPM, "install", installationType) + requirement + ) + return buf1 to buf2 +} diff --git a/utbot-js/src/main/kotlin/utils/JsCmdExec.kt b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt index 6641ddb987..661641217b 100644 --- a/utbot-js/src/main/kotlin/utils/JsCmdExec.kt +++ b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt @@ -1,10 +1,10 @@ package utils +import org.utbot.framework.plugin.api.TimeoutException +import settings.JsTestGenerationSettings.defaultTimeout import java.io.BufferedReader import java.io.File import java.util.concurrent.TimeUnit -import org.utbot.framework.plugin.api.TimeoutException -import settings.JsTestGenerationSettings.defaultTimeout object JsCmdExec { @@ -28,6 +28,6 @@ object JsCmdExec { throw TimeoutException("") } } - return process.inputStream.bufferedReader() to process.errorStream.bufferedReader() + return Pair(process.inputStream.bufferedReader(), process.errorStream.bufferedReader()) } }