diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt index 110c2e121d..2a0e043216 100644 --- a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt @@ -6,7 +6,7 @@ import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.choice import mu.KotlinLogging import org.utbot.cli.js.JsUtils.makeAbsolutePath -import service.CoverageMode +import service.coverage.CoverageMode import settings.JsDynamicSettings import settings.JsExportsSettings.endComment import settings.JsExportsSettings.startComment @@ -82,6 +82,7 @@ class JsGenerateTestsCommand : val sourceFileAbsolutePath = makeAbsolutePath(sourceCodeFile) logger.info { "Generating tests for [$sourceFileAbsolutePath] - started" } val fileText = File(sourceCodeFile).readText() + currentFileText = fileText val outputAbsolutePath = output?.let { makeAbsolutePath(it) } val testGenerator = JsTestGenerator( fileText = fileText, @@ -118,36 +119,31 @@ class JsGenerateTestsCommand : } } - private fun manageExports(exports: List) { - val exportSection = exports.joinToString("\n") { "exports.$it = $it" } + // Needed for continuous exports managing + private var currentFileText = "" + + private fun manageExports(swappedText: (String?, String) -> String) { val file = File(sourceCodeFile) - val fileText = file.readText() when { - fileText.contains(exportSection) -> {} - fileText.contains(startComment) && !fileText.contains(exportSection) -> { + currentFileText.contains(startComment) -> { val regex = Regex("$startComment((\\r\\n|\\n|\\r|.)*)$endComment") - regex.find(fileText)?.groups?.get(1)?.value?.let { existingSection -> - val exportRegex = Regex("exports[.](.*) =") - val existingExports = existingSection.split("\n").filter { it.contains(exportRegex) } - val existingExportsSet = existingExports.map { rawLine -> - exportRegex.find(rawLine)?.groups?.get(1)?.value ?: throw IllegalStateException() - }.toSet() - val resultSet = existingExportsSet + exports.toSet() - val resSection = resultSet.joinToString("\n") { "exports.$it = $it" } - val swappedText = fileText.replace(existingSection, "\n$resSection\n") - file.writeText(swappedText) + regex.find(currentFileText)?.groups?.get(1)?.value?.let { existingSection -> + val newText = swappedText(existingSection, currentFileText) + file.writeText(newText) + currentFileText = newText } } else -> { val line = buildString { - append("\n$startComment\n") - append(exportSection) - append("\n$endComment") + append("\n$startComment") + append(swappedText(null, currentFileText)) + append(endComment) } file.appendText(line) + currentFileText = file.readText() } } } -} \ No newline at end of file +} diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/CoverageModeButtons.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/CoverageModeButtons.kt index f44ca170d7..874a3f1fe9 100644 --- a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/CoverageModeButtons.kt +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/CoverageModeButtons.kt @@ -1,8 +1,8 @@ package org.utbot.intellij.plugin.language.js -import service.CoverageMode import javax.swing.ButtonGroup import javax.swing.JRadioButton +import service.coverage.CoverageMode object CoverageModeButtons { 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 3a1f9bbfd1..e695e347a0 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 @@ -144,6 +144,13 @@ object JsDialogProcessor { private fun createDialog(jsTestsModel: JsTestsModel?) = jsTestsModel?.let { JsDialogWindow(it) } + private fun unblockDocument(project: Project, document: Document) { + PsiDocumentManager.getInstance(project).apply { + commitDocument(document) + doPostponedOperationsAndUnblockDocument(document) + } + } + private fun createTests(model: JsTestsModel, containingFilePath: String, editor: Editor?, contents: String) { val normalizedContainingFilePath = containingFilePath.replace(File.separator, "/") (object : Task.Backgroundable(model.project, "Generate tests") { @@ -154,7 +161,9 @@ object JsDialogProcessor { model.testSourceRoot!! ) val testFileName = normalizedContainingFilePath.substringAfterLast("/").replace(Regex(".js"), "Test.js") - val testGenerator = JsTestGenerator(fileText = contents, + currentFileText = model.file.getContent() + val testGenerator = JsTestGenerator( + fileText = contents, sourceFilePath = normalizedContainingFilePath, projectPath = model.project.basePath?.replace(File.separator, "/") ?: throw IllegalStateException("Can't access project path."), @@ -208,44 +217,6 @@ object JsDialogProcessor { private fun JSFile.getContent(): String = this.viewProvider.contents.toString() - private fun manageExports( - editor: Editor?, project: Project, model: JsTestsModel, exports: List - ) { - AppExecutorUtil.getAppExecutorService().submit { - invokeLater { - val exportSection = exports.joinToString("\n") { "exports.$it = $it" } - val fileText = model.file.getContent() - when { - fileText.contains(exportSection) -> {} - - fileText.contains(startComment) && !fileText.contains(exportSection) -> { - val regex = Regex("$startComment((\\r\\n|\\n|\\r|.)*)$endComment") - regex.find(fileText)?.groups?.get(1)?.value?.let { existingSection -> - val exportRegex = Regex("exports[.](.*) =") - val existingExports = existingSection.split("\n").filter { it.contains(exportRegex) } - val existingExportsSet = existingExports.map { rawLine -> - exportRegex.find(rawLine)?.groups?.get(1)?.value ?: throw IllegalStateException() - }.toSet() - val resultSet = existingExportsSet + exports.toSet() - val resSection = resultSet.joinToString("\n") { "exports.$it = $it" } - val swappedText = fileText.replace(existingSection, "\n$resSection\n") - project.setNewText(editor, model.containingFilePath, swappedText) - } - } - - else -> { - val line = buildString { - append("\n$startComment\n") - append(exportSection) - append("\n$endComment") - } - project.setNewText(editor, model.containingFilePath, fileText + line) - } - } - } - } - } - private fun Project.setNewText(editor: Editor?, filePath: String, text: String) { editor?.let { runWriteAction { @@ -261,12 +232,38 @@ object JsDialogProcessor { } ?: run { File(filePath).writeText(text) } + currentFileText = text } - private fun unblockDocument(project: Project, document: Document) { - PsiDocumentManager.getInstance(project).apply { - commitDocument(document) - doPostponedOperationsAndUnblockDocument(document) + // Needed for continuous exports managing + private var currentFileText = "" + + private fun manageExports( + editor: Editor?, + project: Project, + model: JsTestsModel, + swappedText: (String?, String) -> String + ) { + AppExecutorUtil.getAppExecutorService().submit { + invokeLater { + when { + currentFileText.contains(startComment) -> { + val regex = Regex("$startComment((\\r\\n|\\n|\\r|.)*)$endComment") + regex.find(currentFileText)?.groups?.get(1)?.value?.let { existingSection -> + val newText = swappedText(existingSection, currentFileText) + project.setNewText(editor, model.containingFilePath, newText) + } + } + + else -> { + val line = buildString { + append("\n") + appendLine(swappedText(null, currentFileText)) + } + project.setNewText(editor, model.containingFilePath, currentFileText + line) + } + } + } } } } diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt index fb869d835b..8c8508953d 100644 --- a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt @@ -6,7 +6,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import org.utbot.framework.codegen.domain.TestFramework import org.utbot.intellij.plugin.models.BaseTestsModel -import service.CoverageMode +import service.coverage.CoverageMode import settings.JsTestGenerationSettings.defaultTimeout class JsTestsModel( diff --git a/utbot-js/build.gradle.kts b/utbot-js/build.gradle.kts index 0c365b0be5..0dfcdc95c0 100644 --- a/utbot-js/build.gradle.kts +++ b/utbot-js/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") api(project(":utbot-framework")) - api(project(":utbot-fuzzers")) // https://mvnrepository.com/artifact/com.google.javascript/closure-compiler implementation("com.google.javascript:closure-compiler:v20221102") diff --git a/utbot-js/samples/arrays.js b/utbot-js/samples/arrays.js new file mode 100644 index 0000000000..0b062aa79f --- /dev/null +++ b/utbot-js/samples/arrays.js @@ -0,0 +1,30 @@ +function simpleArray(arr) { + if (arr[0] === 5) { + return 5 + } + return 1 +} + +simpleArray([0, 2]) + +class ObjectParameter { + + constructor(a) { + this.first = a + } + + performAction(value) { + return 2 * value + } +} + +function arrayOfObjects(arr) { + if (arr[0].first === 2) { + return 1 + } + return 10 +} + +let arr = [] +arr[0] = new ObjectParameter(10) +arrayOfObjects(arr) \ No newline at end of file diff --git a/utbot-js/samples/mapStructure.js b/utbot-js/samples/mapStructure.js new file mode 100644 index 0000000000..8007e13d19 --- /dev/null +++ b/utbot-js/samples/mapStructure.js @@ -0,0 +1,11 @@ +// Maps in JavaScript are untyped, so only maps with basic key/value types are feasible to support +function simpleMap(map, compareValue) { + if (map.get("a") === compareValue) { + return 5 + } + return 1 +} + +const map1 = new Map() +map1.set("b", 3.0) +simpleMap(map1, 5) \ No newline at end of file diff --git a/utbot-js/samples/setStructure.js b/utbot-js/samples/setStructure.js new file mode 100644 index 0000000000..92c4b68c3c --- /dev/null +++ b/utbot-js/samples/setStructure.js @@ -0,0 +1,12 @@ +// Sets in JavaScript are untyped, so only sets with basic value types are feasible to support +function setTest(set, checkValue) { + if (set.has(checkValue)) { + return 5 + } + return 1 +} + +let s = new Set() +s.add(5) +s.add(6) +setTest(s, 4) \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt index fa6d76d5ec..a72adaaa82 100644 --- a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt +++ b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt @@ -5,9 +5,13 @@ import com.google.javascript.rhino.Node import framework.api.js.JsClassId import framework.api.js.JsMethodId import framework.api.js.JsUtFuzzedExecution +import framework.api.js.util.isClass +import framework.api.js.util.isJsArray import framework.api.js.util.isJsBasic import framework.api.js.util.jsErrorClassId import framework.api.js.util.jsUndefinedClassId +import framework.codegen.JsImport +import framework.codegen.ModuleType import fuzzer.JsFeedback import fuzzer.JsFuzzingExecutionFeedback import fuzzer.JsMethodDescription @@ -31,8 +35,7 @@ import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtTimeoutException import org.utbot.fuzzing.Control import org.utbot.fuzzing.utils.Trie -import parser.JsClassAstVisitor -import parser.JsFunctionAstVisitor +import parser.JsAstScrapper import parser.JsFuzzerAstVisitor import parser.JsParserUtils import parser.JsParserUtils.getAbstractFunctionName @@ -42,22 +45,26 @@ import parser.JsParserUtils.getClassName import parser.JsParserUtils.getParamName import parser.JsParserUtils.runParser import parser.JsToplevelFunctionAstVisitor -import service.CoverageMode -import service.CoverageServiceProvider +import providers.exports.IExportsProvider import service.InstrumentationService +import service.PackageJson import service.PackageJsonService import service.ServiceContext import service.TernService +import service.coverage.CoverageMode +import service.coverage.CoverageServiceProvider import settings.JsDynamicSettings -import settings.JsTestGenerationSettings.dummyClassName +import settings.JsTestGenerationSettings.fileUnderTestAliases import settings.JsTestGenerationSettings.fuzzingThreshold import settings.JsTestGenerationSettings.fuzzingTimeout import utils.PathResolver -import utils.ResultData import utils.constructClass +import utils.data.ResultData import utils.toJsAny import java.io.File import java.util.concurrent.CancellationException +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.startComment private val logger = KotlinLogging.logger {} @@ -68,7 +75,7 @@ class JsTestGenerator( private val selectedMethods: List? = null, private var parentClassName: String? = null, private var outputFilePath: String?, - private val exportsManager: (List) -> Unit, + private val exportsManager: ((String?, String) -> String) -> Unit, private val settings: JsDynamicSettings, private val isCancelled: () -> Boolean = { false } ) { @@ -77,6 +84,8 @@ class JsTestGenerator( private lateinit var parsedFile: Node + private lateinit var astScrapper: JsAstScrapper + private val utbotDir = "utbotJs" init { @@ -94,10 +103,11 @@ class JsTestGenerator( */ fun run(): String { parsedFile = runParser(fileText) + astScrapper = JsAstScrapper(parsedFile, sourceFilePath) val context = ServiceContext( utbotDir = utbotDir, projectPath = projectPath, - filePathToInference = sourceFilePath, + filePathToInference = astScrapper.filesToInfer, parsedFile = parsedFile, settings = settings, ) @@ -105,7 +115,6 @@ class JsTestGenerator( sourceFilePath, projectPath, ).findClosestConfig() - val ternService = TernService(context) val paramNames = mutableMapOf>() val testSets = mutableListOf() val classNode = @@ -115,17 +124,32 @@ class JsTestGenerator( strict = selectedMethods?.isNotEmpty() ?: false ) parentClassName = classNode?.getClassName() - val classId = makeJsClassId(classNode, ternService) + val classId = makeJsClassId(classNode, TernService(context)) val methods = makeMethodsToTest() if (methods.isEmpty()) throw IllegalArgumentException("No methods to test were found!") methods.forEach { funcNode -> makeTestsForMethod(classId, funcNode, classNode, context, testSets, paramNames) } val importPrefix = makeImportPrefix() + val moduleType = ModuleType.fromPackageJson(context.packageJson) + val imports = listOf( + JsImport( + "*", + fileUnderTestAliases, + "./$importPrefix/${sourceFilePath.substringAfterLast("/")}", + moduleType + ), + JsImport( + "*", + "assert", + "assert", + moduleType + ) + ) val codeGen = JsCodeGenerator( classUnderTest = classId, paramNames = paramNames, - importPrefix = importPrefix + imports = imports ) return codeGen.generateAsStringWithTestReport(testSets).generatedCode } @@ -141,7 +165,7 @@ class JsTestGenerator( val execId = classId.allMethods.find { it.name == funcNode.getAbstractFunctionName() } ?: throw IllegalStateException() - manageExports(classNode, funcNode, execId) + manageExports(classNode, funcNode, execId, context.packageJson) val executionResults = mutableListOf() try { runBlockingWithCancellationPredicate(isCancelled) { @@ -258,7 +282,7 @@ class JsTestGenerator( getUtModelResult( execId = execId, resultData = resultData, - params + jsDescription.thisInstance?.let { params.drop(1) } ?: params ) if (result is UtTimeoutException) { emit(JsTimeoutExecution(result)) @@ -305,12 +329,34 @@ class JsTestGenerator( private fun manageExports( classNode: Node?, funcNode: Node, - execId: JsMethodId + execId: JsMethodId, + packageJson: PackageJson ) { val obligatoryExport = (classNode?.getClassName() ?: funcNode.getAbstractFunctionName()).toString() val collectedExports = collectExports(execId) + val exportsProvider = IExportsProvider.providerByPackageJson(packageJson) exports += (collectedExports + obligatoryExport) - exportsManager(exports.toList()) + exportsManager { existingSection, currentFileText -> + val existingExportsSet = existingSection?.let { section -> + val trimmedSection = section.substringAfter(exportsProvider.exportsPrefix) + .substringBeforeLast(exportsProvider.exportsPostfix) + val exportRegex = exportsProvider.exportsRegex + val existingExports = trimmedSection.split(exportsProvider.exportsDelimiter) + .filter { it.contains(exportRegex) && it.isNotBlank() } + existingExports.map { rawLine -> + exportRegex.find(rawLine)?.groups?.get(1)?.value ?: throw IllegalStateException() + }.toSet() + } ?: emptySet() + val resultSet = existingExportsSet + exports.toSet() + val resSection = resultSet.joinToString( + separator = exportsProvider.exportsDelimiter, + prefix = startComment + exportsProvider.exportsPrefix, + postfix = exportsProvider.exportsPostfix + endComment, + ) { + exportsProvider.getExportsFrame(it) + } + existingSection?.let { currentFileText.replace(startComment + existingSection + endComment, resSection) } ?: resSection + } } private fun makeMethodsToTest(): List { @@ -341,23 +387,25 @@ class JsTestGenerator( } private fun collectExports(methodId: JsMethodId): List { - val res = mutableListOf() - methodId.parameters.forEach { - if (!it.isJsBasic) { - res += it.name - } + return (listOf(methodId.returnType) + methodId.parameters).flatMap { it.collectExportsRecursively() } + } + + private fun JsClassId.collectExportsRecursively(): List { + return when { + this.isClass -> listOf(this.name) + (this.constructor?.parameters ?: emptyList()) + .flatMap { it.collectExportsRecursively() } + + this.isJsArray -> (this.elementClassId as? JsClassId)?.collectExportsRecursively() ?: emptyList() + else -> emptyList() } - if (!methodId.returnType.isJsBasic) res += methodId.returnType.name - return res } private fun getFunctionNode(focusedMethodName: String, parentClassName: String?): Node { - val visitor = JsFunctionAstVisitor( - focusedMethodName, - if (parentClassName != dummyClassName) parentClassName else null - ) - visitor.accept(parsedFile) - return visitor.targetFunctionNode + return parentClassName?.let { astScrapper.findMethod(parentClassName, focusedMethodName, parsedFile) } + ?: astScrapper.findFunction(focusedMethodName, parsedFile) + ?: throw IllegalStateException( + "Couldn't locate function \"$focusedMethodName\" with class ${parentClassName ?: ""}" + ) } private fun getMethodsToTest() = @@ -368,9 +416,7 @@ class JsTestGenerator( } private fun getClassMethods(className: String): List { - val visitor = JsClassAstVisitor(className) - visitor.accept(parsedFile) - val classNode = JsParserUtils.searchForClassDecl(className, parsedFile) + val classNode = astScrapper.findClass(className, parsedFile) return classNode?.getClassMethods() ?: throw IllegalStateException("Can't extract methods of class $className") } } diff --git a/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt index 1087b56e29..ae67e0f929 100644 --- a/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt +++ b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt @@ -2,12 +2,12 @@ package codegen import framework.api.js.JsClassId import framework.codegen.JsCgLanguageAssistant +import framework.codegen.JsImport import framework.codegen.Mocha import org.utbot.framework.codegen.CodeGeneratorResult import org.utbot.framework.codegen.domain.ForceStaticMocking import org.utbot.framework.codegen.domain.HangingTestsTimeout import org.utbot.framework.codegen.domain.ParametrizedTestSource -import org.utbot.framework.codegen.domain.RegularImport import org.utbot.framework.codegen.domain.RuntimeExceptionTestsBehaviour import org.utbot.framework.codegen.domain.StaticsMocking import org.utbot.framework.codegen.domain.TestFramework @@ -20,7 +20,6 @@ import org.utbot.framework.codegen.tree.CgSimpleTestClassConstructor import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.ExecutableId import org.utbot.framework.plugin.api.MockFramework -import settings.JsTestGenerationSettings.fileUnderTestAliases class JsCodeGenerator( private val classUnderTest: JsClassId, @@ -30,7 +29,7 @@ class JsCodeGenerator( hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), enableTestsTimeout: Boolean = true, testClassPackageName: String = classUnderTest.packageName, - importPrefix: String, + imports: List, ) { private var context: CgContext = CgContext( classUnderTest = classUnderTest, @@ -47,13 +46,7 @@ class JsCodeGenerator( hangingTestsTimeout = hangingTestsTimeout, enableTestsTimeout = enableTestsTimeout, testClassPackageName = testClassPackageName, - collectedImports = mutableSetOf( - RegularImport("assert", "assert"), - RegularImport( - fileUnderTestAliases, - "./$importPrefix/${classUnderTest.filePath.substringAfterLast("/")}" - ) - ) + collectedImports = imports.toMutableSet() ) fun generateAsStringWithTestReport( diff --git a/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt b/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt index a54d7879de..6ac73a0048 100644 --- a/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt +++ b/utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt @@ -3,6 +3,9 @@ package framework.api.js.util import framework.api.js.JsClassId import framework.api.js.JsMultipleClassId import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.doubleClassId import org.utbot.framework.plugin.api.util.floatClassId @@ -31,11 +34,35 @@ fun ClassId.toJsClassId() = else -> jsUndefinedClassId } +fun JsClassId.defaultJsValueModel(): UtModel = when (this) { + jsNumberClassId -> UtPrimitiveModel(0.0) + jsDoubleClassId -> UtPrimitiveModel(Double.POSITIVE_INFINITY) + jsBooleanClassId -> UtPrimitiveModel(false) + jsStringClassId -> UtPrimitiveModel("default") + jsUndefinedClassId -> UtPrimitiveModel(0.0) + else -> UtNullModel(this) +} + val JsClassId.isJsBasic: Boolean get() = this in jsBasic || this is JsMultipleClassId +val JsClassId.isExportable: Boolean + get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure) + val JsClassId.isClass: Boolean - get() = !(this.isJsBasic || this == jsErrorClassId) + get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure) val JsClassId.isUndefined: Boolean - get() = this == jsUndefinedClassId \ No newline at end of file + get() = this == jsUndefinedClassId + +val JsClassId.isJsArray: Boolean + get() = this.name == "array" && this.elementClassId is JsClassId + +val JsClassId.isJsMap: Boolean + get() = this.name == "Map" + +val JsClassId.isJsSet: Boolean + get() = this.name == "Set" + +val JsClassId.isJsStdStructure: Boolean + get() = this.isJsArray || this.isJsSet || this.isJsMap diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt index 9e84e24f1d..6584241428 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt @@ -6,7 +6,9 @@ import org.utbot.framework.plugin.api.ClassId import framework.api.js.JsClassId import framework.api.js.util.jsErrorClassId import framework.api.js.util.jsUndefinedClassId +import org.utbot.framework.codegen.domain.Import import org.utbot.framework.codegen.domain.TestFramework +import service.PackageJson object Mocha : TestFramework(id = "Mocha", displayName = "Mocha") { @@ -74,3 +76,27 @@ internal val jsAssertThrows by lazy { ) ) } + +enum class ModuleType { + MODULE, + COMMONJS; + + companion object { + fun fromPackageJson(packageJson: PackageJson): ModuleType { + return when (packageJson.isModule) { + true -> MODULE + else -> COMMONJS + } + } + } +} + +data class JsImport( + val name: String, + val aliases: String, + val path: String, + val type: ModuleType +): Import(2) { + + override val qualifiedName: String = "$name as $aliases from $path" +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt index 098babf9e0..e95cc06b86 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt @@ -1,25 +1,177 @@ package framework.codegen.model.constructor.tree +import framework.api.js.JsClassId +import framework.api.js.JsNullModel import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isExportable +import framework.api.js.util.jsBooleanClassId +import framework.api.js.util.jsDoubleClassId +import framework.api.js.util.jsNumberClassId +import framework.api.js.util.jsStringClassId +import framework.api.js.util.jsUndefinedClassId import org.utbot.framework.codegen.domain.context.CgContext +import org.utbot.framework.codegen.domain.models.CgAllocateArray +import org.utbot.framework.codegen.domain.models.CgArrayInitializer +import org.utbot.framework.codegen.domain.models.CgDeclaration +import org.utbot.framework.codegen.domain.models.CgExpression import org.utbot.framework.codegen.domain.models.CgLiteral import org.utbot.framework.codegen.domain.models.CgValue +import org.utbot.framework.codegen.domain.models.CgVariable +import org.utbot.framework.codegen.tree.CgComponents import org.utbot.framework.codegen.tree.CgVariableConstructor +import org.utbot.framework.codegen.util.at +import org.utbot.framework.codegen.util.inc +import org.utbot.framework.codegen.util.lessThan import org.utbot.framework.codegen.util.nullLiteral +import org.utbot.framework.codegen.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.util.defaultValueModel +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set class JsCgVariableConstructor(ctx: CgContext) : CgVariableConstructor(ctx) { + + private val nameGenerator = CgComponents.getNameGeneratorBy(context) override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { - return if (model is UtReferenceModel) valueByModelId.getOrPut(model.id) { + return if (model is UtAssembleModel) valueByModelId.getOrPut(model.id) { // TODO SEVERE: May lead to unexpected behavior in case of changes to the original method super.getOrCreateVariable(model, name) } else valueByModel.getOrPut(model) { + val baseName = name ?: nameGenerator.nameFrom(model.classId) when (model) { is JsPrimitiveModel -> CgLiteral(model.classId, model.value) + is UtArrayModel -> constructArray(model, baseName) else -> nullLiteral() } } } + + private val MAX_ARRAY_INITIALIZER_SIZE = 10 + + private operator fun UtArrayModel.get(index: Int): UtModel = stores[index] ?: constModel + + private val defaultByPrimitiveType: Map = mapOf( + jsBooleanClassId to false, + jsStringClassId to "default", + jsUndefinedClassId to 0.0, + jsNumberClassId to 0.0, + jsDoubleClassId to Double.POSITIVE_INFINITY + ) + + private infix fun UtModel.isNotJsDefaultValueOf(type: JsClassId): Boolean = !(this isJsDefaultValueOf type) + + private infix fun UtModel.isJsDefaultValueOf(type: JsClassId): Boolean = when (this) { + is JsNullModel -> type.isExportable + is JsPrimitiveModel -> value == defaultByPrimitiveType[type] + else -> false + } + + private fun CgVariable.setArrayElement(index: Any, value: CgValue) { + val i = index.resolve() + this.at(i) `=` value + } + + private fun basicForLoop(until: Any, body: (i: CgExpression) -> Unit) { + basicForLoop(start = 0, until, body) + } + + private fun basicForLoop(start: Any, until: Any, body: (i: CgExpression) -> Unit) { + forLoop { + val (i, init) = loopInitialization(jsNumberClassId, "i", start.resolve()) + initialization = init + condition = i lessThan until.resolve() + update = i.inc() + statements = block { body(i) } + } + } + + private fun loopInitialization( + variableType: ClassId, + baseVariableName: String, + initializer: Any? + ): Pair { + val declaration = CgDeclaration(variableType, baseVariableName.toVarName(), initializer.resolve()) + val variable = declaration.variable + updateVariableScope(variable) + return variable to declaration + } + + private fun constructArray(arrayModel: UtArrayModel, baseName: String?): CgVariable { + val elementType = arrayModel.classId.elementClassId!! as JsClassId + val elementModels = (0 until arrayModel.length).map { + arrayModel.stores.getOrDefault(it, arrayModel.constModel) + } + + val allPrimitives = elementModels.all { it is JsPrimitiveModel } + val allNulls = elementModels.all { it is JsNullModel } + // we can use array initializer if all elements are primitives or all of them are null, + // and the size of an array is not greater than the fixed maximum size + val canInitWithValues = (allPrimitives || allNulls) && elementModels.size <= MAX_ARRAY_INITIALIZER_SIZE + + val initializer = if (canInitWithValues) { + val elements = elementModels.map { model -> + when (model) { + is JsPrimitiveModel -> model.value.resolve() + is UtNullModel -> null.resolve() + else -> error("Non primitive or null model $model is unexpected in array initializer") + } + } + CgArrayInitializer(arrayModel.classId, elementType, elements) + } else { + CgAllocateArray(arrayModel.classId, elementType, arrayModel.length) + } + + val array = newVar(arrayModel.classId, baseName) { initializer } + valueByModelId[arrayModel.id] = array + + if (canInitWithValues) { + return array + } + + if (arrayModel.length <= 0) return array + if (arrayModel.length == 1) { + // take first element value if it is present, otherwise use default value from model + val elementModel = arrayModel[0] + if (elementModel isNotJsDefaultValueOf elementType) { + array.setArrayElement(0, getOrCreateVariable(elementModel)) + } + } else { + val indexedValuesFromStores = + if (arrayModel.stores.size == arrayModel.length) { + // do not use constModel because stores fully cover array + arrayModel.stores.entries.filter { (_, element) -> element isNotJsDefaultValueOf elementType } + } else { + // fill array if constModel is not default type value + if (arrayModel.constModel isNotJsDefaultValueOf elementType) { + val defaultVariable = getOrCreateVariable(arrayModel.constModel, "defaultValue") + basicForLoop(arrayModel.length) { i -> + array.setArrayElement(i, defaultVariable) + } + } + + // choose all not default values + val defaultValue = if (arrayModel.constModel isJsDefaultValueOf elementType) { + arrayModel.constModel + } else { + elementType.defaultValueModel() + } + arrayModel.stores.entries.filter { (_, element) -> element != defaultValue } + } + + // set all values from stores manually + indexedValuesFromStores + .sortedBy { it.key } + .forEach { (index, element) -> array.setArrayElement(index, getOrCreateVariable(element)) } + } + + return array + } + + private fun String.toVarName(): String = nameGenerator.variableName(this) } diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt index 45715897df..428c55b833 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt @@ -1,5 +1,9 @@ package framework.codegen.model.constructor.visitor +import framework.api.js.JsClassId +import framework.api.js.util.isExportable +import framework.codegen.JsImport +import framework.codegen.ModuleType import org.apache.commons.text.StringEscapeUtils import org.utbot.framework.codegen.domain.RegularImport import org.utbot.framework.codegen.domain.StaticImport @@ -109,6 +113,10 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP else -> "$this" } + override fun renderRegularImport(regularImport: RegularImport) { + println("const ${regularImport.packageName} = require(\"${regularImport.className}\")") + } + override fun visit(element: CgStaticsRegion) { if (element.content.isEmpty()) return @@ -196,13 +204,14 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP override fun visit(element: CgArrayInitializer) { val elementType = element.elementType val elementsInLine = arrayElementsInLine(elementType) - + print("[") element.values.renderElements(elementsInLine) + print("]") } override fun visit(element: CgClassFile) { - element.imports.filterIsInstance().forEach { - renderRegularImport(it) + element.imports.filterIsInstance().forEach { + renderImport(it) } println() element.declaredClass.accept(this) @@ -247,14 +256,20 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP } override fun visit(element: CgConstructorCall) { - print("new $fileUnderTestAliases.${element.executableId.classId.name}") + val importPrefix = "$fileUnderTestAliases.".takeIf { + (element.executableId.classId as JsClassId).isExportable + } ?: "" + print("new $importPrefix${element.executableId.classId.name}") print("(") element.arguments.renderSeparated() print(")") } - override fun renderRegularImport(regularImport: RegularImport) { - println("const ${regularImport.packageName} = require(\"${regularImport.className}\")") + private fun renderImport(import: JsImport) = with(import) { + when (type) { + ModuleType.COMMONJS -> println("const $aliases = require(\"$path\")") + ModuleType.MODULE -> println("import $name as $aliases from \"$path\"") + } } override fun renderStaticImport(staticImport: StaticImport) { diff --git a/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt b/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt index 024aa860b8..c9953083d8 100644 --- a/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt +++ b/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt @@ -1,9 +1,12 @@ package fuzzer import framework.api.js.JsClassId +import fuzzer.providers.ArrayValueProvider import fuzzer.providers.BoolValueProvider +import fuzzer.providers.MapValueProvider import fuzzer.providers.NumberValueProvider import fuzzer.providers.ObjectValueProvider +import fuzzer.providers.SetValueProvider import fuzzer.providers.StringValueProvider import org.utbot.framework.plugin.api.UtModel import org.utbot.fuzzing.Fuzzing @@ -14,7 +17,10 @@ fun defaultValueProviders() = listOf( BoolValueProvider, NumberValueProvider, StringValueProvider, - ObjectValueProvider() + MapValueProvider, + SetValueProvider, + ObjectValueProvider(), + ArrayValueProvider() ) class JsFuzzing( diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/ArrayValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/ArrayValueProvider.kt new file mode 100644 index 0000000000..ab966c29d2 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/ArrayValueProvider.kt @@ -0,0 +1,38 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.util.defaultJsValueModel +import framework.api.js.util.isJsArray +import fuzzer.JsIdProvider +import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +class ArrayValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean = type.isJsArray + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtArrayModel( + id = JsIdProvider.createId(), + classId = type, + length = it, + constModel = (type.elementClassId!! as JsClassId).defaultJsValueModel(), + stores = hashMapOf(), + ) + }, + modify = Routine.ForEach(listOf(type.elementClassId!! as JsClassId)) { self, i, values -> + (self as UtArrayModel).stores[i] = values.first() + } + )) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/BoolValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/BoolValueProvider.kt index e4d8f9246e..87031b6559 100644 --- a/utbot-js/src/main/kotlin/fuzzer/providers/BoolValueProvider.kt +++ b/utbot-js/src/main/kotlin/fuzzer/providers/BoolValueProvider.kt @@ -1,6 +1,5 @@ package fuzzer.providers - import framework.api.js.JsClassId import framework.api.js.JsPrimitiveModel import framework.api.js.util.isJsBasic diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/MapValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/MapValueProvider.kt new file mode 100644 index 0000000000..79122fa6b5 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/MapValueProvider.kt @@ -0,0 +1,58 @@ +package fuzzer.providers + + +import framework.api.js.JsClassId +import framework.api.js.JsMethodId +import framework.api.js.util.isJsMap +import framework.api.js.util.jsUndefinedClassId +import fuzzer.JsIdProvider +import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +object MapValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean = type.isJsMap + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtAssembleModel( + id = JsIdProvider.createId(), + classId = type, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(type, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ) + }, + modify = Routine.ForEach(listOf(jsUndefinedClassId, jsUndefinedClassId)) { self, _, values -> + val model = self as UtAssembleModel + model.modificationsChain as MutableList += + UtExecutableCallModel( + model, + JsMethodId( + classId = type, + name = "set", + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = listOf(jsUndefinedClassId, jsUndefinedClassId) + ), + values + ) + } + ) + ) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/ObjectValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/ObjectValueProvider.kt index 12ea5e6ab6..7370654808 100644 --- a/utbot-js/src/main/kotlin/fuzzer/providers/ObjectValueProvider.kt +++ b/utbot-js/src/main/kotlin/fuzzer/providers/ObjectValueProvider.kt @@ -9,6 +9,8 @@ import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtNullModel + + import org.utbot.fuzzing.Routine import org.utbot.fuzzing.Seed import org.utbot.fuzzing.ValueProvider diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/SetValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/SetValueProvider.kt new file mode 100644 index 0000000000..6991518c9f --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/SetValueProvider.kt @@ -0,0 +1,57 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsMethodId +import framework.api.js.util.isJsSet +import framework.api.js.util.jsUndefinedClassId +import fuzzer.JsIdProvider +import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Routine +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +object SetValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean = type.isJsSet + + override fun generate( + description: JsMethodDescription, + type: JsClassId + ) = sequence> { + yield( + Seed.Collection( + construct = Routine.Collection { + UtAssembleModel( + id = JsIdProvider.createId(), + classId = type, + modelName = "", + instantiationCall = UtExecutableCallModel( + null, + ConstructorId(type, emptyList()), + emptyList() + ), + modificationsChainProvider = { mutableListOf() } + ) + }, + modify = Routine.ForEach(listOf(jsUndefinedClassId)) { self, _, values -> + val model = self as UtAssembleModel + model.modificationsChain as MutableList += + UtExecutableCallModel( + model, + JsMethodId( + classId = type, + name = "add", + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = listOf(jsUndefinedClassId) + ), + values + ) + } + ) + ) + } +} diff --git a/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt b/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt new file mode 100644 index 0000000000..a22b232cf2 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt @@ -0,0 +1,149 @@ +package parser + +import com.google.javascript.jscomp.Compiler +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.jscomp.SourceFile +import com.google.javascript.rhino.Node +import java.io.File +import java.nio.file.Paths +import mu.KotlinLogging +import parser.JsParserUtils.getAbstractFunctionName +import parser.JsParserUtils.getClassMethods +import parser.JsParserUtils.getImportSpecAliases +import parser.JsParserUtils.getImportSpecName +import parser.JsParserUtils.getModuleImportSpecsAsList +import parser.JsParserUtils.getModuleImportText +import parser.JsParserUtils.getRequireImportText +import parser.JsParserUtils.isRequireImport +import kotlin.io.path.pathString + +private val logger = KotlinLogging.logger {} + +class JsAstScrapper( + private val parsedFile: Node, + private val basePath: String, +) { + + // Used not to parse the same file multiple times. + private val _parsedFilesCache = mutableMapOf() + private val _filesToInfer: MutableList = mutableListOf(basePath) + val filesToInfer: List + get() = _filesToInfer.toList() + private val _importsMap = mutableMapOf() + val importsMap: Map + get() = _importsMap.toMap() + + init { + _importsMap.apply { + val visitor = Visitor() + visitor.accept(parsedFile) + val res = visitor.importNodes.fold(emptyMap()) { acc, node -> + val currAcc = acc.toList().toTypedArray() + val more = node.importedNodes().toList().toTypedArray() + mapOf(*currAcc, *more) + } + this.putAll(res) + this.toMap() + } + } + + fun findFunction(key: String, file: Node): Node? { + if (_importsMap[key]?.isFunction == true) return _importsMap[key] + val functionVisitor = JsFunctionAstVisitor(key, null) + functionVisitor.accept(file) + return try { + functionVisitor.targetFunctionNode + } catch (e: Exception) { null } + } + + fun findClass(key: String, file: Node): Node? { + if (_importsMap[key]?.isClass == true) return _importsMap[key] + val classVisitor = JsClassAstVisitor(key) + classVisitor.accept(file) + return try { + classVisitor.targetClassNode + } catch (e: Exception) { null } + } + + fun findMethod(classKey: String, methodKey: String, file: Node): Node? { + val classNode = findClass(classKey, file) + return classNode?.getClassMethods()?.find { it.getAbstractFunctionName() == methodKey } + } + + private fun File.parseIfNecessary(): Node = + _parsedFilesCache.getOrPut(this.path) { + _filesToInfer += this.path.replace("\\", "/") + Compiler().parse(SourceFile.fromCode(this.path, readText())) + } + + private fun Node.importedNodes(): Map { + return when { + this.isRequireImport() -> mapOf( + this.parent!!.string to (makePathFromImport(this.getRequireImportText())?.let { + File(it).parseIfNecessary().findEntityInFile(null) + // Workaround for std imports. + } ?: this.firstChild!!.next!!) + ) + this.isImport -> this.processModuleImport() + else -> emptyMap() + } + } + + private fun Node.processModuleImport(): Map { + try { + val pathToFile = makePathFromImport(this.getModuleImportText()) ?: return emptyMap() + val pFile = File(pathToFile).parseIfNecessary() + return when { + NodeUtil.findPreorder(this, { it.isImportSpecs }, { true }) != null -> { + this.getModuleImportSpecsAsList().associate { spec -> + val realName = spec.getImportSpecName() + val aliases = spec.getImportSpecAliases() + aliases to pFile.findEntityInFile(realName) + } + } + NodeUtil.findPreorder(this, { it.isImportStar }, { true }) != null -> { + val aliases = this.getImportSpecAliases() + mapOf(aliases to pFile) + } + // For example: import foo from "bar" + else -> { + val realName = this.getImportSpecName() + mapOf(realName to pFile.findEntityInFile(realName)) + } + } + } catch (e: Exception) { + logger.error { e.toString() } + return emptyMap() + } + } + + private fun makePathFromImport(importText: String): String? { + val relPath = importText + if (importText.endsWith(".js")) "" else ".js" + // If import text doesn't contain "/", then it is NodeJS stdlib import. + if (!relPath.contains("/")) return null + return Paths.get(File(basePath).parent).resolve(Paths.get(relPath)).pathString + } + + private fun Node.findEntityInFile(key: String?): Node { + return key?.let { k -> + findClass(k, this) + ?: findFunction(k, this) + ?: throw ClassNotFoundException("Could not locate entity $k in ${this.sourceFileName}") + } ?: this + } + + private class Visitor: IAstVisitor { + + private val _importNodes = mutableListOf() + + val importNodes: List + get() = _importNodes.toList() + + // TODO: commented for release since features are incomplete + override fun accept(rootNode: Node) { +// NodeUtil.visitPreOrder(rootNode) { node -> +// if (node.isImport || node.isRequireImport()) _importNodes += node +// } + } + } +} diff --git a/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt index d60be4316f..463edcfb5b 100644 --- a/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt +++ b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt @@ -22,26 +22,30 @@ class JsFuzzerAstVisitor : IAstVisitor { NodeUtil.visitPreOrder(rootNode) { node -> val currentFuzzedOp = node.toFuzzedContextComparisonOrNull() when { - node.isCase -> validateNode(node.firstChild?.getAnyValue()) + node.isCase -> validateNode(node.firstChild?.getAnyValue(), JsFuzzedContext.NE) + node.isCall -> { + validateNode(node.getAnyValue(), JsFuzzedContext.NE) + } + currentFuzzedOp != null -> { lastFuzzedOpGlobal = currentFuzzedOp - validateNode(node.getBinaryExprLeftOperand().getAnyValue()) + validateNode(node.getBinaryExprLeftOperand().getAnyValue(), lastFuzzedOpGlobal) lastFuzzedOpGlobal = lastFuzzedOpGlobal.reverse() - validateNode(node.getBinaryExprRightOperand().getAnyValue()) + validateNode(node.getBinaryExprRightOperand().getAnyValue(), lastFuzzedOpGlobal) } } } } - private fun validateNode(value: Any?) { + private fun validateNode(value: Any?, fuzzedOp: JsFuzzedContext) { when (value) { is String -> { fuzzedConcreteValues.add( JsFuzzedConcreteValue( jsStringClassId, value.toString(), - lastFuzzedOpGlobal + fuzzedOp ) ) } @@ -51,13 +55,13 @@ class JsFuzzerAstVisitor : IAstVisitor { JsFuzzedConcreteValue( jsBooleanClassId, value, - lastFuzzedOpGlobal + fuzzedOp ) ) } is Double -> { - fuzzedConcreteValues.add(JsFuzzedConcreteValue(jsDoubleClassId, value, lastFuzzedOpGlobal)) + fuzzedConcreteValues.add(JsFuzzedConcreteValue(jsDoubleClassId, value, fuzzedOp)) } } } diff --git a/utbot-js/src/main/kotlin/parser/JsParserUtils.kt b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt index 8e43e078d4..e1d1df2385 100644 --- a/utbot-js/src/main/kotlin/parser/JsParserUtils.kt +++ b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt @@ -1,6 +1,7 @@ package parser import com.google.javascript.jscomp.Compiler +import com.google.javascript.jscomp.NodeUtil import com.google.javascript.jscomp.SourceFile import com.google.javascript.rhino.Node import fuzzer.JsFuzzedContext @@ -67,6 +68,12 @@ object JsParserUtils { this.isString -> this.string this.isTrue -> true this.isFalse -> false + this.isCall -> { + if (this.firstChild?.isGetProp == true) { + this.firstChild?.next?.getAnyValue() + } else null + } + else -> null } @@ -144,4 +151,59 @@ object JsParserUtils { * Called upon node with Method token. */ fun Node.isStatic(): Boolean = this.isStaticMember + + /** + * Checks if node is "require" JavaScript import. + */ + fun Node.isRequireImport(): Boolean = try { + this.isCall && this.firstChild?.string == "require" + } catch (e: ClassCastException) { + false + } + + /** + * Called upon "require" JavaScript import. + * + * Returns path to imported file as [String]. + */ + fun Node.getRequireImportText(): String = this.firstChild!!.next!!.string + + /** + * Called upon "import" JavaScript import. + * + * Returns path to imported file as [String]. + */ + fun Node.getModuleImportText(): String = this.firstChild!!.next!!.next!!.string + + /** + * Called upon "import" JavaScript import. + * + * Returns imported objects as [List]. + */ + fun Node.getModuleImportSpecsAsList(): List { + val importSpecsNode = NodeUtil.findPreorder(this, { it.isImportSpecs }, { true }) + ?: throw UnsupportedOperationException("Module import doesn't contain \"import_specs\" token as an AST child") + var currNode: Node? = importSpecsNode.firstChild!! + val importSpecsList = mutableListOf() + do { + importSpecsList += currNode!! + currNode = currNode?.next + } while (currNode?.isImportSpec == true) + return importSpecsList + } + + /** + * Called upon IMPORT_SPEC Node. + * + * Returns name of imported object as [String]. + */ + fun Node.getImportSpecName(): String = this.firstChild!!.string + + /** + * Called upon IMPORT_SPEC Node. + * + * Returns import alias as [String]. + */ + fun Node.getImportSpecAliases(): String = this.firstChild!!.next!!.string + } diff --git a/utbot-js/src/main/kotlin/providers/exports/IExportsProvider.kt b/utbot-js/src/main/kotlin/providers/exports/IExportsProvider.kt new file mode 100644 index 0000000000..895b0a8cea --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/exports/IExportsProvider.kt @@ -0,0 +1,25 @@ +package providers.exports + +import service.PackageJson + +interface IExportsProvider { + + val exportsRegex: Regex + + val exportsDelimiter: String + + fun getExportsFrame(exportString: String): String + + val exportsPrefix: String + + val exportsPostfix: String + + fun instrumentationFunExport(funName: String): String + + companion object { + fun providerByPackageJson(packageJson: PackageJson): IExportsProvider = when (packageJson.isModule) { + true -> ModuleExportsProvider() + else -> RequireExportsProvider() + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/exports/ModuleExportsProvider.kt b/utbot-js/src/main/kotlin/providers/exports/ModuleExportsProvider.kt new file mode 100644 index 0000000000..77fae0fc1e --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/exports/ModuleExportsProvider.kt @@ -0,0 +1,16 @@ +package providers.exports + +class ModuleExportsProvider : IExportsProvider { + + override val exportsDelimiter: String = "," + + override val exportsPostfix: String = "}\n" + + override val exportsPrefix: String = "\nexport {" + + override val exportsRegex: Regex = Regex("(.*)") + + override fun getExportsFrame(exportString: String): String = exportString + + override fun instrumentationFunExport(funName: String): String = "\nexport {$funName}" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/exports/RequireExportsProvider.kt b/utbot-js/src/main/kotlin/providers/exports/RequireExportsProvider.kt new file mode 100644 index 0000000000..644744bd66 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/exports/RequireExportsProvider.kt @@ -0,0 +1,16 @@ +package providers.exports + +class RequireExportsProvider : IExportsProvider { + + override val exportsDelimiter: String = "\n" + + override val exportsPostfix: String = "\n" + + override val exportsPrefix: String = "\n" + + override val exportsRegex: Regex = Regex("exports[.](.*) =") + + override fun getExportsFrame(exportString: String): String = "exports.$exportString = $exportString" + + override fun instrumentationFunExport(funName: String): String = "\nexports.$funName = $funName" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/imports/IImportsProvider.kt b/utbot-js/src/main/kotlin/providers/imports/IImportsProvider.kt new file mode 100644 index 0000000000..c2b821a904 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/imports/IImportsProvider.kt @@ -0,0 +1,18 @@ +package providers.imports + +import service.PackageJson +import service.ServiceContext + +interface IImportsProvider { + + val ternScriptImports: String + + val tempFileImports: String + + companion object { + fun providerByPackageJson(packageJson: PackageJson, context: ServiceContext): IImportsProvider = when (packageJson.isModule) { + true -> ModuleImportsProvider(context) + else -> RequireImportsProvider(context) + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt b/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt new file mode 100644 index 0000000000..7d1aa55c01 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt @@ -0,0 +1,22 @@ +package providers.imports + +import service.ContextOwner +import service.ServiceContext +import settings.JsTestGenerationSettings.fileUnderTestAliases + +class ModuleImportsProvider(context: ServiceContext) : IImportsProvider, ContextOwner by context { + + override val ternScriptImports: String = buildString { + appendLine("import * as tern from \"tern/lib/tern.js\"") + appendLine("import * as condense from \"tern/lib/condense.js\"") + appendLine("import * as util from \"tern/test/util.js\"") + appendLine("import * as fs from \"fs\"") + appendLine("import * as path from \"path\"") + } + + override val tempFileImports: String = buildString { + val importFileUnderTest = "./instr/${filePathToInference.first().substringAfterLast("/")}" + appendLine("import * as $fileUnderTestAliases from \"$importFileUnderTest\"") + appendLine("import * as fs from \"fs\"") + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt b/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt new file mode 100644 index 0000000000..d653b2b047 --- /dev/null +++ b/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt @@ -0,0 +1,23 @@ +package providers.imports + +import service.ContextOwner +import service.ServiceContext +import settings.JsTestGenerationSettings.fileUnderTestAliases + +class RequireImportsProvider(context: ServiceContext) : IImportsProvider, ContextOwner by context { + + override val ternScriptImports: String = buildString { + appendLine("const tern = require(\"tern/lib/tern\")") + appendLine("const condense = require(\"tern/lib/condense.js\")") + appendLine("const util = require(\"tern/test/util.js\")") + appendLine("const fs = require(\"fs\")") + appendLine("const path = require(\"path\")") + } + + override val tempFileImports: String = buildString { + val importFileUnderTest = "instr/${filePathToInference.first().substringAfterLast("/")}" + appendLine("const $fileUnderTestAliases = require(\"./$importFileUnderTest\")") + appendLine("const fs = require(\"fs\")\n") + } + +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/InstrumentationService.kt b/utbot-js/src/main/kotlin/service/InstrumentationService.kt index dc81a71f09..e36dd0e41d 100644 --- a/utbot-js/src/main/kotlin/service/InstrumentationService.kt +++ b/utbot-js/src/main/kotlin/service/InstrumentationService.kt @@ -1,19 +1,29 @@ package service +import com.google.javascript.jscomp.CodePrinter import com.google.javascript.jscomp.NodeUtil import com.google.javascript.rhino.Node import org.apache.commons.io.FileUtils import parser.JsFunctionAstVisitor import parser.JsParserUtils.getAnyValue +import parser.JsParserUtils.getRequireImportText +import parser.JsParserUtils.isRequireImport import parser.JsParserUtils.runParser import utils.JsCmdExec +import utils.PathResolver.getRelativePath import java.io.File +import java.nio.file.Paths +import parser.JsParserUtils.getModuleImportText +import providers.exports.IExportsProvider +import kotlin.io.path.pathString import kotlin.math.roundToInt -class InstrumentationService(context: ServiceContext, private val funcDeclOffset: Pair): ContextOwner by context { +class InstrumentationService(context: ServiceContext, private val funcDeclOffset: Pair) : + ContextOwner by context { private val destinationFolderPath = "${projectPath}/${utbotDir}/instr" - private val instrumentedFilePath = "$destinationFolderPath/${filePathToInference.substringAfterLast("/")}" + private val instrumentedFilePath = "$destinationFolderPath/${filePathToInference.first().substringAfterLast("/")}" + private lateinit var parsedInstrFile: Node lateinit var covFunName: String val allStatements: Set @@ -92,10 +102,8 @@ class InstrumentationService(context: ServiceContext, private val funcDeclOffset } private fun getStatementMapKeys() = buildSet { - val fileText = File(instrumentedFilePath).readText() - val rootNode = runParser(fileText) val funcVisitor = JsFunctionAstVisitor(covFunName, null) - funcVisitor.accept(rootNode) + funcVisitor.accept(parsedInstrFile) val funcNode = funcVisitor.targetFunctionNode val funcLocation = getFuncLocation(funcNode) funcNode.findAndIterateOver("statementMap") { currKey -> @@ -117,7 +125,7 @@ class InstrumentationService(context: ServiceContext, private val funcDeclOffset covFuncNode.findAndIterateOver("fnMap") { currKey -> val declLocation = currKey!!.getObjectLocation("decl") if (funcDeclOffset == declLocation.start) { - result = currKey.getObjectLocation("loc") + result = currKey.getObjectLocation("loc") return@findAndIterateOver } } @@ -125,24 +133,60 @@ class InstrumentationService(context: ServiceContext, private val funcDeclOffset } fun instrument() { - val fileName = filePathToInference.substringAfterLast("/") + val fileName = filePathToInference.first().substringAfterLast("/") JsCmdExec.runCommand( cmd = arrayOf(settings.pathToNYC, "instrument", fileName, destinationFolderPath), - dir = filePathToInference.substringBeforeLast("/"), + dir = filePathToInference.first().substringBeforeLast("/"), shouldWait = true, timeout = settings.timeout, ) val instrumentedFileText = File(instrumentedFilePath).readText() + parsedInstrFile = runParser(instrumentedFileText) val covFunRegex = Regex("function (cov_.*)\\(\\).*") val funName = covFunRegex.find(instrumentedFileText.takeWhile { it != '{' })?.groups?.get(1)?.value ?: throw IllegalStateException("") - val fixedFileText = "$instrumentedFileText\nexports.$funName = $funName" - File(instrumentedFilePath).writeText(fixedFileText) + val fixedFileText = fixImportsInInstrumentedFile() + + IExportsProvider.providerByPackageJson(packageJson).instrumentationFunExport(funName) + File(instrumentedFilePath).writeTextAndUpdate(fixedFileText) covFunName = funName } + private fun File.writeTextAndUpdate(newText: String) { + this.writeText(newText) + parsedInstrFile = runParser(File(instrumentedFilePath).readText()) + } + + private fun fixImportsInInstrumentedFile(): String { + // nyc poorly handles imports paths in file to instrument. Manual fix required. + NodeUtil.visitPreOrder(parsedInstrFile) { node -> + when { + node.isRequireImport() -> { + val currString = node.getRequireImportText() + val relPath = Paths.get( + getRelativePath( + "${projectPath}/${utbotDir}/instr", + File(filePathToInference.first()).parent + ) + ).resolve(currString).pathString.replace("\\", "/") + node.firstChild!!.next!!.string = relPath + } + node.isImport -> { + val currString = node.getModuleImportText() + val relPath = Paths.get( + getRelativePath( + "${projectPath}/${utbotDir}/instr", + File(filePathToInference.first()).parent + ) + ).resolve(currString).pathString.replace("\\", "/") + node.firstChild!!.next!!.next!!.string = relPath + } + } + } + return CodePrinter.Builder(parsedInstrFile).build() + } + fun removeTempFiles() { FileUtils.deleteDirectory(File("$projectPath/$utbotDir/instr")) } diff --git a/utbot-js/src/main/kotlin/service/ServiceContext.kt b/utbot-js/src/main/kotlin/service/ServiceContext.kt index 29315e139d..d30ccf1fea 100644 --- a/utbot-js/src/main/kotlin/service/ServiceContext.kt +++ b/utbot-js/src/main/kotlin/service/ServiceContext.kt @@ -6,7 +6,7 @@ import settings.JsDynamicSettings class ServiceContext( override val utbotDir: String, override val projectPath: String, - override val filePathToInference: String, + override val filePathToInference: List, override val parsedFile: Node, override val settings: JsDynamicSettings, override var packageJson: PackageJson = PackageJson.defaultConfig @@ -15,7 +15,7 @@ class ServiceContext( interface ContextOwner { val utbotDir: String val projectPath: String - val filePathToInference: String + val filePathToInference: List val parsedFile: Node val settings: JsDynamicSettings var packageJson: PackageJson diff --git a/utbot-js/src/main/kotlin/service/TernService.kt b/utbot-js/src/main/kotlin/service/TernService.kt index 6e8f0271fe..faf526d0d1 100644 --- a/utbot-js/src/main/kotlin/service/TernService.kt +++ b/utbot-js/src/main/kotlin/service/TernService.kt @@ -11,30 +11,21 @@ import parser.JsParserUtils.getAbstractFunctionName import parser.JsParserUtils.getAbstractFunctionParams import parser.JsParserUtils.getClassName import parser.JsParserUtils.getConstructor +import providers.imports.IImportsProvider import utils.JsCmdExec -import utils.MethodTypes import utils.constructClass +import utils.data.MethodTypes import java.io.File -import java.util.Locale - -/* - NOTE: this approach is quite bad, but we failed to implement alternatives. - TODO: 1. MINOR: Find a better solution after the first stable version. - 2. SEVERE: Load all necessary .js files in Tern.js since functions can be exported and used in other files. - */ /** * Installs and sets up scripts for running Tern.js type guesser. */ class TernService(context: ServiceContext) : ContextOwner by context { + private val importProvider = IImportsProvider.providerByPackageJson(packageJson, context) private fun ternScriptCode() = """ -const tern = require("tern/lib/tern") -const condense = require("tern/lib/condense.js") -const util = require("tern/test/util.js") -const fs = require("fs") -const path = require("path") +${generateImportsSection()} var condenseDir = ""; @@ -60,30 +51,23 @@ function runTest(options) { } function test(options) { - if (typeof options == "string") options = {load: [options]}; + options = {load: options}; runTest(options); } -test("$filePathToInference") +test(["${filePathToInference.joinToString(separator = "\", \"")}"]) """ init { with(context) { setupTernEnv("$projectPath/$utbotDir") - installDeps("$projectPath/$utbotDir") runTypeInferencer() } } private lateinit var json: JSONObject - private fun installDeps(path: String) { - JsCmdExec.runCommand( - dir = path, - shouldWait = true, - cmd = arrayOf("\"${settings.pathToNPM}\"", "i", "tern", "-l"), - ) - } + private fun generateImportsSection(): String = importProvider.ternScriptImports private fun setupTernEnv(path: String) { File(path).mkdirs() @@ -109,7 +93,7 @@ test("$filePathToInference") return try { val classJson = json.getJSONObject(classNode.getClassName()) val constructorFunc = classJson.getString("!type") - .filterNot { setOf(' ', '+', '!').contains(it) } + .filterNot { setOf(' ', '+').contains(it) } extractParameters(constructorFunc) } catch (e: JSONException) { classNode.getConstructor()?.getAbstractFunctionParams()?.map { jsUndefinedClassId } ?: emptyList() @@ -120,10 +104,11 @@ test("$filePathToInference") val parametersRegex = Regex("fn[(](.+)[)]") return parametersRegex.find(line)?.groups?.get(1)?.let { matchResult -> val value = matchResult.value - val paramList = value.split(',') - paramList.map { param -> - val paramReg = Regex(":(.*)") + val paramGroupList = Regex("(\\w+:\\[\\w+(,\\w+)*]|\\w+:\\w+)|\\w+:\\?").findAll(value).toList() + paramGroupList.map { paramGroup -> + val paramReg = Regex("\\w*:(.*)") try { + val param = paramGroup.groups[0]!!.value makeClassId( paramReg.find(param)?.groups?.get(1)?.value ?: throw IllegalStateException() @@ -160,7 +145,7 @@ test("$filePathToInference") } val methodJson = scope.getJSONObject(funcNode.getAbstractFunctionName()) val typesString = methodJson.getString("!type") - .filterNot { setOf(' ', '+', '!').contains(it) } + .filterNot { setOf(' ', '+').contains(it) } val parametersList = lazy { extractParameters(typesString) } val returnType = lazy { extractReturnType(typesString) } @@ -173,21 +158,20 @@ test("$filePathToInference") } } - //TODO MINOR: move to appropriate place (JsIdUtil or JsClassId constructor) private fun makeClassId(name: String): JsClassId { val classId = when { - // TODO SEVERE: I don't know why Tern sometimes says that type is "0" - name == "?" || name.toIntOrNull() != null -> jsUndefinedClassId + name == "?" || name.toIntOrNull() != null || name.contains('!') -> jsUndefinedClassId Regex("\\[(.*)]").matches(name) -> { - val arrType = Regex("\\[(.*)]").find(name)?.groups?.get(1)?.value ?: throw IllegalStateException() + val arrType = Regex("\\[(.*)]").find(name)?.groups?.get(1)?.value?.substringBefore(",") + ?: throw IllegalStateException() JsClassId( jsName = "array", elementClassId = makeClassId(arrType) ) } - name.contains('|') -> JsMultipleClassId(name.lowercase(Locale.getDefault())) - else -> JsClassId(name.lowercase(Locale.getDefault())) + name.contains('|') -> JsMultipleClassId(name) + else -> JsClassId(name) } return try { diff --git a/utbot-js/src/main/kotlin/service/BasicCoverageService.kt b/utbot-js/src/main/kotlin/service/coverage/BasicCoverageService.kt similarity index 92% rename from utbot-js/src/main/kotlin/service/BasicCoverageService.kt rename to utbot-js/src/main/kotlin/service/coverage/BasicCoverageService.kt index a5c5f9482a..5ba1facadd 100644 --- a/utbot-js/src/main/kotlin/service/BasicCoverageService.kt +++ b/utbot-js/src/main/kotlin/service/coverage/BasicCoverageService.kt @@ -1,20 +1,21 @@ -package service +package service.coverage import mu.KotlinLogging import org.json.JSONObject import org.utbot.framework.plugin.api.TimeoutException +import service.ServiceContext import settings.JsTestGenerationSettings.tempFileName import utils.JsCmdExec -import utils.ResultData +import utils.data.ResultData import java.io.File private val logger = KotlinLogging.logger {} class BasicCoverageService( context: ServiceContext, + baseCoverage: Map, private val scriptTexts: List, - baseCoverage: List, -) : CoverageService(context, scriptTexts, baseCoverage) { +) : CoverageService(context, baseCoverage, scriptTexts) { override fun generateCoverageReport() { scriptTexts.indices.forEach { index -> diff --git a/utbot-js/src/main/kotlin/service/CoverageMode.kt b/utbot-js/src/main/kotlin/service/coverage/CoverageMode.kt similarity index 65% rename from utbot-js/src/main/kotlin/service/CoverageMode.kt rename to utbot-js/src/main/kotlin/service/coverage/CoverageMode.kt index a1f0c0d9cc..5fee242ac0 100644 --- a/utbot-js/src/main/kotlin/service/CoverageMode.kt +++ b/utbot-js/src/main/kotlin/service/coverage/CoverageMode.kt @@ -1,4 +1,4 @@ -package service +package service.coverage enum class CoverageMode { FAST, diff --git a/utbot-js/src/main/kotlin/service/CoverageService.kt b/utbot-js/src/main/kotlin/service/coverage/CoverageService.kt similarity index 73% rename from utbot-js/src/main/kotlin/service/CoverageService.kt rename to utbot-js/src/main/kotlin/service/coverage/CoverageService.kt index a5bfd0425b..b51106e8cf 100644 --- a/utbot-js/src/main/kotlin/service/CoverageService.kt +++ b/utbot-js/src/main/kotlin/service/coverage/CoverageService.kt @@ -1,19 +1,20 @@ -package service +package service.coverage import java.io.File -import java.util.Collections import org.json.JSONException import org.json.JSONObject +import service.ContextOwner +import service.ServiceContext import settings.JsTestGenerationSettings -import utils.CoverageData import utils.JsCmdExec -import utils.ResultData +import utils.data.CoverageData +import utils.data.ResultData abstract class CoverageService( context: ServiceContext, + private val baseCoverage: Map, private val scriptTexts: List, - private val baseCoverage: List, -): ContextOwner by context { +) : ContextOwner by context { private val _utbotDirPath = lazy { "${projectPath}/${utbotDir}" } protected val utbotDirPath: String @@ -31,7 +32,7 @@ abstract class CoverageService( file.createNewFile() } - fun getBaseCoverage(context: ServiceContext, baseCoverageScriptText: String): List { + fun getBaseCoverage(context: ServiceContext, baseCoverageScriptText: String): Map { with(context) { val utbotDirPath = "${projectPath}/${utbotDir}" createTempScript( @@ -39,16 +40,18 @@ abstract class CoverageService( scriptText = baseCoverageScriptText ) JsCmdExec.runCommand( - cmd = arrayOf("\"${settings.pathToNode}\"", "\"$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.js\""), + cmd = arrayOf( + "\"${settings.pathToNode}\"", + "\"$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.js\"" + ), dir = projectPath, shouldWait = true, timeout = settings.timeout, ) return JSONObject(File("$utbotDirPath/${JsTestGenerationSettings.tempFileName}Base.json").readText()) - .getJSONObject("s").let { - it.keySet().flatMap { key -> - val count = it.getInt(key) - Collections.nCopies(count, key.toInt()) + .getJSONObject("s").let { obj -> + obj.keySet().associate { key -> + key.toInt() to obj.getInt(key) } } } @@ -74,17 +77,13 @@ abstract class CoverageService( // TODO: sort by coverage size desc return coverageList .map { (_, obj) -> - val dirtyCoverage = obj - .let { - it.keySet().flatMap { key -> - val count = it.getInt(key) - Collections.nCopies(count, key.toInt()) - }.toMutableList() - } - baseCoverage.forEach { - dirtyCoverage.remove(it) + val map = obj.keySet().associate { key -> + val intKey = key.toInt() + intKey to (obj.getInt(key) - baseCoverage.getOrDefault(intKey, 0)) } - CoverageData(dirtyCoverage.toSet()) + CoverageData(map.mapNotNull { entry -> + entry.key.takeIf { entry.value > 0 } + }.toSet()) } } catch (e: JSONException) { throw Exception("Could not get coverage of test cases!") @@ -95,7 +94,6 @@ abstract class CoverageService( abstract fun generateCoverageReport() - private fun createTempScript(path: String, scriptText: String) { val file = File(path) file.writeText(scriptText) @@ -109,4 +107,4 @@ abstract class CoverageService( File("$utbotDirPath/${JsTestGenerationSettings.tempFileName}$index.js").delete() } } -} \ No newline at end of file +} diff --git a/utbot-js/src/main/kotlin/service/CoverageServiceProvider.kt b/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt similarity index 68% rename from utbot-js/src/main/kotlin/service/CoverageServiceProvider.kt rename to utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt index 0a879df01d..7d027d1d91 100644 --- a/utbot-js/src/main/kotlin/service/CoverageServiceProvider.kt +++ b/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt @@ -1,16 +1,25 @@ -package service +package service.coverage +import framework.api.js.JsClassId import framework.api.js.JsMethodId import framework.api.js.JsPrimitiveModel +import framework.api.js.util.isExportable import framework.api.js.util.isUndefined import fuzzer.JsMethodDescription +import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.util.isStatic +import providers.imports.IImportsProvider +import service.ContextOwner +import service.InstrumentationService +import service.ServiceContext import settings.JsTestGenerationSettings import settings.JsTestGenerationSettings.tempFileName -import utils.CoverageData -import utils.ResultData +import utils.data.CoverageData +import utils.data.ResultData import java.util.regex.Pattern class CoverageServiceProvider( @@ -20,11 +29,7 @@ class CoverageServiceProvider( private val description: JsMethodDescription ) : ContextOwner by context { - private val importFileUnderTest = "instr/${filePathToInference.substringAfterLast("/")}" - - private val imports = - "const ${JsTestGenerationSettings.fileUnderTestAliases} = require(\"./$importFileUnderTest\")\n" + - "const fs = require(\"fs\")\n\n" + private val imports = IImportsProvider.providerByPackageJson(packageJson, context).tempFileImports private val filePredicate = """ function check_value(value, json) { @@ -42,7 +47,7 @@ function check_value(value, json) { } """ - private val baseCoverage: List + private val baseCoverage: Map init { val temp = makeScriptForBaseCoverage( @@ -89,8 +94,8 @@ function check_value(value, json) { } val coverageService = BasicCoverageService( context = context, + baseCoverage = baseCoverage, scriptTexts = tempScriptTexts, - baseCoverage = baseCoverage ) coverageService.generateCoverageReport() return coverageService.getCoveredLines() to coverageService.resultList @@ -113,9 +118,9 @@ function check_value(value, json) { } val coverageService = FastCoverageService( context = context, + baseCoverage = baseCoverage, scriptTexts = listOf(tempScriptTexts), testCaseIndices = fuzzedValues.indices, - baseCoverage = baseCoverage, ) coverageService.generateCoverageReport() return coverageService.getCoveredLines() to coverageService.resultList @@ -139,7 +144,7 @@ fs.writeFileSync("$resFilePath", JSON.stringify(json)) index: Int, resFilePath: String, ): String { - val callString = makeCallFunctionString(fuzzedValue, method, containingClass) + val callString = makeCallFunctionString(fuzzedValue, method, containingClass, index) return """ let json$index = {} json$index.is_inf = false @@ -148,7 +153,7 @@ json$index.is_error = false json$index.spec_sign = 1 let res$index try { - res$index = $callString + $callString check_value(res$index, json$index) } catch(e) { res$index = e.message @@ -166,21 +171,34 @@ fs.writeFileSync("$resFilePath$index.json", JSON.stringify(json$index)) private fun makeCallFunctionString( fuzzedValue: List, method: JsMethodId, - containingClass: String? + containingClass: String?, + index: Int ): String { + val paramsInit = initParams(fuzzedValue) val actualParams = description.thisInstance?.let { fuzzedValue.drop(1) } ?: fuzzedValue val initClass = containingClass?.let { if (!method.isStatic) { - description.thisInstance?.let { fuzzedValue[0].toCallString() } + description.thisInstance?.let { fuzzedValue[0].initModelAsString() } ?: "new ${JsTestGenerationSettings.fileUnderTestAliases}.${it}()" } else "${JsTestGenerationSettings.fileUnderTestAliases}.$it" } ?: JsTestGenerationSettings.fileUnderTestAliases var callString = "$initClass.${method.name}" - callString += actualParams.joinToString( - prefix = "(", + callString = List(actualParams.size) { idx -> "param$idx" }.joinToString( + prefix = "res$index = $callString(", postfix = ")", - ) { value -> value.toCallString() } - return callString + ) + return paramsInit + callString + } + + private fun initParams(fuzzedValue: List): String { + val actualParams = description.thisInstance?.let { fuzzedValue.drop(1) } ?: fuzzedValue + return actualParams.mapIndexed { index, param -> + val varName = "param$index" + buildString { + appendLine("let $varName = ${param.initModelAsString()}") + (param as? UtAssembleModel)?.initModificationsAsString(this, varName) + } + }.joinToString(separator = "\n") } private fun Any.quoteWrapIfNecessary(): String = @@ -198,22 +216,54 @@ fs.writeFileSync("$resFilePath$index.json", JSON.stringify(json$index)) } private fun UtAssembleModel.toParamString(): String { - val callConstructorString = "new ${JsTestGenerationSettings.fileUnderTestAliases}.${classId.name}" + val importPrefix = "new ${JsTestGenerationSettings.fileUnderTestAliases}.".takeIf { + (classId as JsClassId).isExportable + } ?: "new " + val callConstructorString = importPrefix + classId.name val paramsString = instantiationCall.params.joinToString( prefix = "(", postfix = ")", ) { - it.toCallString() + it.initModelAsString() } return callConstructorString + paramsString } + private fun UtArrayModel.toParamString(): String { + val paramsString = stores.values.joinToString( + prefix = "[", + postfix = "]", + ) { + it.initModelAsString() + } + return paramsString + } - private fun UtModel.toCallString(): String = + private fun UtModel.initModelAsString(): String = when (this) { is UtAssembleModel -> this.toParamString() + is UtArrayModel -> this.toParamString() + is UtNullModel -> "null" else -> { (this as JsPrimitiveModel).value.escapeSymbolsIfNecessary().quoteWrapIfNecessary() } } + + private fun UtAssembleModel.initModificationsAsString(stringBuilder: StringBuilder, varName: String) { + with(stringBuilder) { + this@initModificationsAsString.modificationsChain.forEach { + if (it is UtExecutableCallModel) { + val exec = it.executable as JsMethodId + appendLine( + it.params.joinToString( + prefix = "$varName.${exec.name}(", + postfix = ")" + ) { model -> + model.initModelAsString() + } + ) + } + } + } + } } diff --git a/utbot-js/src/main/kotlin/service/FastCoverageService.kt b/utbot-js/src/main/kotlin/service/coverage/FastCoverageService.kt similarity index 90% rename from utbot-js/src/main/kotlin/service/FastCoverageService.kt rename to utbot-js/src/main/kotlin/service/coverage/FastCoverageService.kt index c6f762bd25..32a427cdd9 100644 --- a/utbot-js/src/main/kotlin/service/FastCoverageService.kt +++ b/utbot-js/src/main/kotlin/service/coverage/FastCoverageService.kt @@ -1,21 +1,22 @@ -package service +package service.coverage import mu.KotlinLogging import org.json.JSONObject +import service.ServiceContext import settings.JsTestGenerationSettings.fuzzingThreshold import settings.JsTestGenerationSettings.tempFileName import utils.JsCmdExec -import utils.ResultData +import utils.data.ResultData import java.io.File private val logger = KotlinLogging.logger {} class FastCoverageService( context: ServiceContext, + baseCoverage: Map, scriptTexts: List, private val testCaseIndices: IntRange, - baseCoverage: List, -) : CoverageService(context, scriptTexts, baseCoverage) { +) : CoverageService(context, baseCoverage, scriptTexts) { override fun generateCoverageReport() { val (_, errorText) = JsCmdExec.runCommand( diff --git a/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt b/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt index 81d1ac02a0..c985b2454f 100644 --- a/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt +++ b/utbot-js/src/main/kotlin/settings/JsDynamicSettings.kt @@ -1,6 +1,6 @@ package settings -import service.CoverageMode +import service.coverage.CoverageMode data class JsDynamicSettings( val pathToNode: String = "node", diff --git a/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt index bace7de39b..54149f9202 100644 --- a/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt +++ b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt @@ -30,7 +30,7 @@ fun JsClassId.constructClass( methods = methods, constructor = constructor, classPackagePath = ternService.projectPath, - classFilePath = ternService.filePathToInference, + classFilePath = ternService.filePathToInference.first(), ) methods.forEach { it.classId = newClassId @@ -73,4 +73,4 @@ private fun JsClassId.constructMethods( }.asSequence() return methods } -} \ No newline at end of file +} diff --git a/utbot-js/src/main/kotlin/utils/ValueUtil.kt b/utbot-js/src/main/kotlin/utils/ValueUtil.kt index 2349a7a674..50862cd4b4 100644 --- a/utbot-js/src/main/kotlin/utils/ValueUtil.kt +++ b/utbot-js/src/main/kotlin/utils/ValueUtil.kt @@ -1,5 +1,7 @@ package utils +import org.json.JSONException +import org.json.JSONObject import framework.api.js.JsClassId import framework.api.js.util.jsBooleanClassId import framework.api.js.util.jsDoubleClassId @@ -7,8 +9,7 @@ import framework.api.js.util.jsErrorClassId import framework.api.js.util.jsNumberClassId import framework.api.js.util.jsStringClassId import framework.api.js.util.jsUndefinedClassId -import org.json.JSONException -import org.json.JSONObject +import utils.data.ResultData fun ResultData.toJsAny(returnType: JsClassId = jsUndefinedClassId): Pair { this.buildUniqueValue()?.let { return it } diff --git a/utbot-js/src/main/kotlin/utils/CoverageData.kt b/utbot-js/src/main/kotlin/utils/data/CoverageData.kt similarity index 75% rename from utbot-js/src/main/kotlin/utils/CoverageData.kt rename to utbot-js/src/main/kotlin/utils/data/CoverageData.kt index 6dc2538f73..b64fdfdaf7 100644 --- a/utbot-js/src/main/kotlin/utils/CoverageData.kt +++ b/utbot-js/src/main/kotlin/utils/data/CoverageData.kt @@ -1,5 +1,5 @@ -package utils +package utils.data data class CoverageData( val additionalCoverage: Set -) \ No newline at end of file +) diff --git a/utbot-js/src/main/kotlin/utils/MethodTypes.kt b/utbot-js/src/main/kotlin/utils/data/MethodTypes.kt similarity index 88% rename from utbot-js/src/main/kotlin/utils/MethodTypes.kt rename to utbot-js/src/main/kotlin/utils/data/MethodTypes.kt index 342acdf508..cea1c403ee 100644 --- a/utbot-js/src/main/kotlin/utils/MethodTypes.kt +++ b/utbot-js/src/main/kotlin/utils/data/MethodTypes.kt @@ -1,4 +1,4 @@ -package utils +package utils.data import framework.api.js.JsClassId diff --git a/utbot-js/src/main/kotlin/utils/ResultData.kt b/utbot-js/src/main/kotlin/utils/data/ResultData.kt similarity index 97% rename from utbot-js/src/main/kotlin/utils/ResultData.kt rename to utbot-js/src/main/kotlin/utils/data/ResultData.kt index fe4a274175..ed8e6ef0e1 100644 --- a/utbot-js/src/main/kotlin/utils/ResultData.kt +++ b/utbot-js/src/main/kotlin/utils/data/ResultData.kt @@ -1,4 +1,4 @@ -package utils +package utils.data /** * Represents results after running function with arguments using Node.js