Skip to content

Commit 5db82b4

Browse files
authored
Refactoring the npm installation process for JavaScript (#1891)
* Refactor npm packages checking & installing * Enhanced package installation for JavaScript * Add JSON file consistency check * Add message about time expiration for npm packages installation * Add packageJson in ServiceContext * Text corrections
1 parent 4d95b98 commit 5db82b4

File tree

7 files changed

+222
-102
lines changed

7 files changed

+222
-102
lines changed

utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt

Lines changed: 66 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,15 @@ import org.jetbrains.kotlin.idea.util.application.invokeLater
2323
import org.jetbrains.kotlin.idea.util.application.runReadAction
2424
import org.jetbrains.kotlin.idea.util.application.runWriteAction
2525
import org.jetbrains.kotlin.konan.file.File
26+
import org.utbot.framework.plugin.api.TimeoutException
2627
import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater
2728
import org.utbot.intellij.plugin.ui.utils.testModules
2829
import settings.JsDynamicSettings
2930
import settings.JsExportsSettings.endComment
3031
import settings.JsExportsSettings.startComment
31-
import settings.JsPackagesSettings.mochaData
32-
import settings.JsPackagesSettings.nycData
33-
import settings.JsPackagesSettings.ternData
3432
import settings.JsTestGenerationSettings.dummyClassName
35-
import settings.PackageData
33+
import settings.PackageDataService
34+
import settings.jsPackagesList
3635
import utils.JsCmdExec
3736
import utils.OsProvider
3837
import java.io.IOException
@@ -58,9 +57,10 @@ object JsDialogProcessor {
5857
) {
5958
override fun run(indicator: ProgressIndicator) {
6059
invokeLater {
61-
checkAndInstallRequirement(model.project, model.pathToNPM, mochaData)
62-
checkAndInstallRequirement(model.project, model.pathToNPM, nycData)
63-
checkAndInstallRequirement(model.project, model.pathToNPM, ternData)
60+
if (!PackageDataService(
61+
model.containingFilePath, model.project.basePath!!, model.pathToNPM
62+
).checkAndInstallRequirements(project)
63+
) return@invokeLater
6464
createDialog(model)?.let { dialogWindow ->
6565
if (!dialogWindow.showAndGet()) return@invokeLater
6666
// Since Tern.js accesses containing file, sync with file system required before test generation.
@@ -76,36 +76,31 @@ object JsDialogProcessor {
7676
}).queue()
7777
}
7878

79-
private fun findNodeAndNPM(): Pair<String, String>? =
80-
try {
81-
val pathToNode = NodeJsLocalInterpreterManager.getInstance()
82-
.interpreters.first().interpreterSystemIndependentPath
83-
val (_, errorText) = JsCmdExec.runCommand(
84-
shouldWait = true,
85-
cmd = arrayOf("\"${pathToNode}\"", "-v")
86-
)
87-
if (errorText.isNotEmpty()) throw NoSuchElementException()
88-
val pathToNPM =
89-
pathToNode.substringBeforeLast("/") + "/" + "npm" + OsProvider.getProviderByOs().npmPackagePostfix
90-
pathToNode to pathToNPM
91-
} catch (e: NoSuchElementException) {
92-
Messages.showErrorDialog(
93-
"Node.js interpreter is not found in IDEA settings.\n" +
94-
"Please set it in Settings > Languages & Frameworks > Node.js",
95-
"Requirement Error"
96-
)
97-
logger.error { "Node.js interpreter was not found in IDEA settings." }
98-
null
99-
} catch (e: IOException) {
100-
Messages.showErrorDialog(
101-
"Node.js interpreter path is corrupted in IDEA settings.\n" +
102-
"Please check Settings > Languages & Frameworks > Node.js",
103-
"Requirement Error"
104-
)
105-
logger.error { "Node.js interpreter path is corrupted in IDEA settings." }
106-
null
107-
}
108-
79+
private fun findNodeAndNPM(): Pair<String, String>? = try {
80+
val pathToNode =
81+
NodeJsLocalInterpreterManager.getInstance().interpreters.first().interpreterSystemIndependentPath
82+
val (_, errorText) = JsCmdExec.runCommand(
83+
shouldWait = true, cmd = arrayOf("\"${pathToNode}\"", "-v")
84+
)
85+
if (errorText.isNotEmpty()) throw NoSuchElementException()
86+
val pathToNPM =
87+
pathToNode.substringBeforeLast("/") + "/" + "npm" + OsProvider.getProviderByOs().npmPackagePostfix
88+
pathToNode to pathToNPM
89+
} catch (e: NoSuchElementException) {
90+
Messages.showErrorDialog(
91+
"Node.js interpreter is not found in IDEA settings.\n" + "Please set it in Settings > Languages & Frameworks > Node.js",
92+
"Requirement Error"
93+
)
94+
logger.error { "Node.js interpreter was not found in IDEA settings." }
95+
null
96+
} catch (e: IOException) {
97+
Messages.showErrorDialog(
98+
"Node.js interpreter path is corrupted in IDEA settings.\n" + "Please check Settings > Languages & Frameworks > Node.js",
99+
"Requirement Error"
100+
)
101+
logger.error { "Node.js interpreter path is corrupted in IDEA settings." }
102+
null
103+
}
109104

110105
private fun createJsTestModel(
111106
project: Project,
@@ -138,7 +133,6 @@ object JsDialogProcessor {
138133
this.pathToNode = pathToNode
139134
this.pathToNPM = pathToNPM
140135
}
141-
142136
}
143137

144138
private fun createDialog(jsTestsModel: JsTestsModel?) = jsTestsModel?.let { JsDialogWindow(it) }
@@ -159,10 +153,8 @@ object JsDialogProcessor {
159153
val testDir = PsiDirectoryFactory.getInstance(project).createDirectory(
160154
model.testSourceRoot!!
161155
)
162-
val testFileName = normalizedContainingFilePath.substringAfterLast("/")
163-
.replace(Regex(".js"), "Test.js")
164-
val testGenerator = JsTestGenerator(
165-
fileText = editor.document.text,
156+
val testFileName = normalizedContainingFilePath.substringAfterLast("/").replace(Regex(".js"), "Test.js")
157+
val testGenerator = JsTestGenerator(fileText = editor.document.text,
166158
sourceFilePath = normalizedContainingFilePath,
167159
projectPath = model.project.basePath?.replace(File.separator, "/")
168160
?: throw IllegalStateException("Can't access project path."),
@@ -267,45 +259,47 @@ object JsDialogProcessor {
267259
}
268260
}
269261

270-
fun checkAndInstallRequirement(
271-
project: Project,
272-
pathToNPM: String,
273-
requirement: PackageData,
274-
) {
275-
if (!requirement.findPackageByNpm(project.basePath!!, pathToNPM)) {
276-
installMissingRequirement(project, pathToNPM, requirement)
277-
}
278-
}
279-
280-
private fun installMissingRequirement(
281-
project: Project,
282-
pathToNPM: String,
283-
requirement: PackageData,
284-
) {
262+
private fun PackageDataService.checkAndInstallRequirements(project: Project): Boolean {
263+
val missingPackages = jsPackagesList.filterNot { this.findPackage(it) }
264+
if (missingPackages.isEmpty()) return true
285265
val message = """
286-
Requirement is not installed:
287-
${requirement.packageName}
288-
Install it?
266+
Requirements are not installed:
267+
${missingPackages.joinToString { it.packageName }}
268+
Install them?
289269
""".trimIndent()
290270
val result = Messages.showOkCancelDialog(
291-
project,
292-
message,
293-
"Requirement Missmatch Error",
294-
"Install",
295-
"Cancel",
296-
null
271+
project, message, "Requirements Missmatch Error", "Install", "Cancel", null
297272
)
298273

299274
if (result == Messages.CANCEL)
300-
return
275+
return false
301276

302-
val (_, errorText) = requirement.installPackage(project.basePath!!, pathToNPM)
303-
304-
if (errorText.isNotEmpty()) {
277+
try {
278+
val (_, errorText) = this.installMissingPackages(missingPackages)
279+
if (errorText.isNotEmpty()) {
280+
showErrorDialogLater(
281+
project,
282+
"Requirements installing failed with some reason:\n${errorText}",
283+
"Failed to install requirements"
284+
)
285+
return false
286+
}
287+
return true
288+
} catch (_: TimeoutException) {
305289
showErrorDialogLater(
306290
project,
307-
"Requirements installing failed with some reason:\n${errorText}",
308-
"Requirements error"
291+
"""
292+
Requirements installing failed due to the exceeded waiting time for the installation, check your internet connection.
293+
294+
Try to install missing npm packages manually:
295+
${
296+
missingPackages.joinToString(separator = "\n") {
297+
"> npm install ${it.npmListFlag} ${it.packageName}"
298+
}
299+
}
300+
""".trimIndent(),
301+
"Failed to install requirements"
309302
)
303+
return false
310304
}
311305
}

utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/NycSourceFileChooser.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import com.intellij.openapi.ui.TextBrowseFolderListener
55
import com.intellij.openapi.ui.TextFieldWithBrowseButton
66
import com.intellij.openapi.ui.ValidationInfo
77
import org.utbot.common.PathUtil.replaceSeparator
8-
import settings.JsPackagesSettings.nycData
8+
import settings.PackageDataService
99
import utils.OsProvider
1010

1111

@@ -24,8 +24,7 @@ class NycSourceFileChooser(val model: JsTestsModel) : TextFieldWithBrowseButton(
2424
addBrowseFolderListener(
2525
TextBrowseFolderListener(descriptor, model.project)
2626
)
27-
text = (replaceSeparator(nycData.findPackagePath() ?: "Nyc was not found")
28-
+ OsProvider.getProviderByOs().npmPackagePostfix)
27+
text = PackageDataService.nycPath
2928
}
3029

3130
fun validateNyc(): ValidationInfo? {

utbot-js/src/main/kotlin/api/JsTestGenerator.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.utbot.framework.plugin.api.UtTimeoutException
3030
import org.utbot.fuzzer.FuzzedValue
3131
import org.utbot.fuzzer.UtFuzzedExecution
3232
import org.utbot.fuzzing.Control
33+
import org.utbot.fuzzing.utils.Trie
3334
import parser.JsClassAstVisitor
3435
import parser.JsFunctionAstVisitor
3536
import parser.JsFuzzerAstVisitor
@@ -44,6 +45,7 @@ import parser.JsToplevelFunctionAstVisitor
4445
import service.CoverageMode
4546
import service.CoverageServiceProvider
4647
import service.InstrumentationService
48+
import service.PackageJsonService
4749
import service.ServiceContext
4850
import service.TernService
4951
import settings.JsDynamicSettings
@@ -56,7 +58,6 @@ import utils.constructClass
5658
import utils.toJsAny
5759
import java.io.File
5860
import java.util.concurrent.CancellationException
59-
import org.utbot.fuzzing.utils.Trie
6061

6162
private val logger = KotlinLogging.logger {}
6263

@@ -100,6 +101,10 @@ class JsTestGenerator(
100101
parsedFile = parsedFile,
101102
settings = settings,
102103
)
104+
context.packageJson = PackageJsonService(
105+
sourceFilePath,
106+
projectPath,
107+
).findClosestConfig()
103108
val ternService = TernService(context)
104109
val paramNames = mutableMapOf<ExecutableId, List<String>>()
105110
val testSets = mutableListOf<CgMethodTestSet>()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package service
2+
3+
import org.json.JSONObject
4+
import java.io.File
5+
import java.io.FilenameFilter
6+
7+
data class PackageJson(
8+
val isModule: Boolean,
9+
val deps: Set<String>
10+
) {
11+
companion object {
12+
val defaultConfig = PackageJson(false, emptySet())
13+
}
14+
}
15+
16+
class PackageJsonService(
17+
private val filePathToInference: String,
18+
private val projectPath: String
19+
) {
20+
21+
fun findClosestConfig(): PackageJson {
22+
var currDir = File(filePathToInference.substringBeforeLast("/"))
23+
do {
24+
val matchingFiles: Array<File> = currDir.listFiles(
25+
FilenameFilter { _, name ->
26+
return@FilenameFilter name == "package.json"
27+
}
28+
) ?: throw IllegalStateException("Error occurred while scanning file system")
29+
if (matchingFiles.isNotEmpty()) return parseConfig(matchingFiles.first())
30+
currDir = currDir.parentFile
31+
} while (currDir.path != projectPath)
32+
return PackageJson.defaultConfig
33+
}
34+
35+
private fun parseConfig(configFile: File): PackageJson {
36+
val configAsJson = JSONObject(configFile.readText())
37+
return PackageJson(
38+
isModule = configAsJson.optString("type") == "module",
39+
deps = configAsJson.optJSONObject("dependencies")?.keySet() ?: emptySet()
40+
)
41+
}
42+
}

utbot-js/src/main/kotlin/service/ServiceContext.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ class ServiceContext(
99
override val filePathToInference: String,
1010
override val parsedFile: Node,
1111
override val settings: JsDynamicSettings,
12-
): ContextOwner
12+
override var packageJson: PackageJson = PackageJson.defaultConfig
13+
) : ContextOwner
1314

1415
interface ContextOwner {
1516
val utbotDir: String
1617
val projectPath: String
1718
val filePathToInference: String
1819
val parsedFile: Node
1920
val settings: JsDynamicSettings
20-
}
21+
var packageJson: PackageJson
22+
}

0 commit comments

Comments
 (0)