Skip to content

Added Node JS getter from IDEA setting with automatically NPM packages installation #1566

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

Merged
merged 3 commits into from
Dec 22, 2022
Merged
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
1 change: 1 addition & 0 deletions utbot-intellij-js/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {

Expand All @@ -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<JSMemberInfo>,
focusedMethod: JSMemberInfo?,
filePath: String,
file: JSFile
): JsDialogWindow? {
): JsTestsModel? {
val testModules = srcModule.testModules(project)

if (testModules.isEmpty()) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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"
)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 =
Expand All @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,3 +41,15 @@ fun findFrameworkLibrary(npmPackageName: String, model: JsTestsModel): Boolean {
}
return checkForPackageText.contains(npmPackageName)
}

fun installRequirement(pathToNPM: String, requirement: String, installingDir: String?): Pair<BufferedReader, BufferedReader> {
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
}
6 changes: 3 additions & 3 deletions utbot-js/src/main/kotlin/utils/JsCmdExec.kt
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -28,6 +28,6 @@ object JsCmdExec {
throw TimeoutException("")
}
}
return process.inputStream.bufferedReader() to process.errorStream.bufferedReader()
return Pair(process.inputStream.bufferedReader(), process.errorStream.bufferedReader())
}
}