diff --git a/utbot-js/samples/commonIfStatement.js b/utbot-js/samples/commonIfStatement.js index 94b6880a7e..addde25d70 100644 --- a/utbot-js/samples/commonIfStatement.js +++ b/utbot-js/samples/commonIfStatement.js @@ -1,4 +1,4 @@ -function foo(a,b) { +function foo(a, b) { if (a > 10) { return a * b } else { diff --git a/utbot-js/samples/mapStructure.js b/utbot-js/samples/mapStructure.js index 8007e13d19..1cbf515b69 100644 --- a/utbot-js/samples/mapStructure.js +++ b/utbot-js/samples/mapStructure.js @@ -8,4 +8,4 @@ function simpleMap(map, compareValue) { const map1 = new Map() map1.set("b", 3.0) -simpleMap(map1, 5) \ No newline at end of file +simpleMap(map1, 5) diff --git a/utbot-js/samples/multi_file_tests/basic_import/basicImport.js b/utbot-js/samples/multi_file_tests/basic_import/basicImport.js new file mode 100644 index 0000000000..57109bfab0 --- /dev/null +++ b/utbot-js/samples/multi_file_tests/basic_import/basicImport.js @@ -0,0 +1,7 @@ +const {ObjectParameter} = require("./toImport.js") + +function test(obj) { + return obj.performAction(5) +} + +test(new ObjectParameter(5)) diff --git a/utbot-js/samples/multi_file_tests/basic_import/toImport.js b/utbot-js/samples/multi_file_tests/basic_import/toImport.js new file mode 100644 index 0000000000..55ff185b31 --- /dev/null +++ b/utbot-js/samples/multi_file_tests/basic_import/toImport.js @@ -0,0 +1,12 @@ +class ObjectParameter { + + constructor(a) { + this.first = a + } + + performAction(value) { + return 2 * value + } +} + +exports.ObjectParameter = ObjectParameter diff --git a/utbot-js/samples/multi_file_tests/chain_imports/chainInports.js b/utbot-js/samples/multi_file_tests/chain_imports/chainInports.js new file mode 100644 index 0000000000..5439a440c1 --- /dev/null +++ b/utbot-js/samples/multi_file_tests/chain_imports/chainInports.js @@ -0,0 +1,7 @@ +const {glob} = require("./toImport.js") + +function test(obj) { + return obj.performAction(5) +} + +test(glob) diff --git a/utbot-js/samples/multi_file_tests/chain_imports/temp.js b/utbot-js/samples/multi_file_tests/chain_imports/temp.js new file mode 100644 index 0000000000..93aa4c7201 --- /dev/null +++ b/utbot-js/samples/multi_file_tests/chain_imports/temp.js @@ -0,0 +1,8 @@ +class Some { + + constructor(b) { + this.b = b + } +} + +exports.Some = Some diff --git a/utbot-js/samples/multi_file_tests/chain_imports/toImport.js b/utbot-js/samples/multi_file_tests/chain_imports/toImport.js new file mode 100644 index 0000000000..2a2dadb5a2 --- /dev/null +++ b/utbot-js/samples/multi_file_tests/chain_imports/toImport.js @@ -0,0 +1,18 @@ +const {Some} = require("./temp.js") + +class ObjectParameter { + + constructor(some) { + this.first = some.b + } + + performAction(value) { + return 2 * value + } +} + +// Using global variable to "hide" actual class (Some) from the chainImports.js file +let glob = new ObjectParameter(new Some(5)) + +exports.glob = glob +exports.ObjectParameter = ObjectParameter diff --git a/utbot-js/samples/scenarioMultyClassNoTopLevel.js b/utbot-js/samples/scenarioMultyClassNoTopLevel.js index 7f6973db3c..9687c5f64c 100644 --- a/utbot-js/samples/scenarioMultyClassNoTopLevel.js +++ b/utbot-js/samples/scenarioMultyClassNoTopLevel.js @@ -1,4 +1,4 @@ -class Na { +class Double { constructor(num) { this.num = num } @@ -6,13 +6,9 @@ class Na { double() { return this.num * 2 } - - static test(a, b) { - return a + 2 * b - } } -class Kek { +class Functions { foo(a, b) { return a + b } diff --git a/utbot-js/samples/scenarioThrowError.js b/utbot-js/samples/scenarioThrowError.js index 2068695dd1..fb5faabfe4 100644 --- a/utbot-js/samples/scenarioThrowError.js +++ b/utbot-js/samples/scenarioThrowError.js @@ -1,11 +1,10 @@ function functionToTest(a) { if (a === true) { - throw Error("err") + throw Error("MyCustomError") } else if (a === 1) { while (true) { } } else { return -1 } - } diff --git a/utbot-js/samples/setStructure.js b/utbot-js/samples/setStructure.js index 92c4b68c3c..db6a004af8 100644 --- a/utbot-js/samples/setStructure.js +++ b/utbot-js/samples/setStructure.js @@ -9,4 +9,4 @@ function setTest(set, checkValue) { let s = new Set() s.add(5) s.add(6) -setTest(s, 4) \ No newline at end of file +setTest(s, 4) diff --git a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt index d008ed1391..0d960808d9 100644 --- a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt +++ b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt @@ -4,6 +4,7 @@ import codegen.JsCodeGenerator import com.google.javascript.rhino.Node import framework.api.js.JsClassId import framework.api.js.JsMethodId +import framework.api.js.JsMultipleClassId import framework.api.js.JsUtFuzzedExecution import framework.api.js.util.isClass import framework.api.js.util.isJsArray @@ -36,7 +37,7 @@ import org.utbot.framework.plugin.api.UtTimeoutException import org.utbot.fuzzing.Control import org.utbot.fuzzing.utils.Trie import parser.JsAstScrapper -import parser.JsFuzzerAstVisitor +import parser.visitors.JsFuzzerAstVisitor import parser.JsParserUtils import parser.JsParserUtils.getAbstractFunctionName import parser.JsParserUtils.getAbstractFunctionParams @@ -44,7 +45,7 @@ import parser.JsParserUtils.getClassMethods import parser.JsParserUtils.getClassName import parser.JsParserUtils.getParamName import parser.JsParserUtils.runParser -import parser.JsToplevelFunctionAstVisitor +import parser.visitors.JsToplevelFunctionAstVisitor import providers.exports.IExportsProvider import service.InstrumentationService import service.PackageJson @@ -62,6 +63,7 @@ import utils.constructClass import utils.data.ResultData import utils.toJsAny import java.io.File +import java.nio.file.Paths import java.util.concurrent.CancellationException import settings.JsExportsSettings.endComment import settings.JsExportsSettings.startComment @@ -102,19 +104,28 @@ class JsTestGenerator( * Returns String representation of generated tests. */ fun run(): String { - parsedFile = runParser(fileText) - astScrapper = JsAstScrapper(parsedFile, sourceFilePath) + parsedFile = runParser(fileText, sourceFilePath) + val packageJson = PackageJsonService( + sourceFilePath, + File(projectPath), + ).findClosestConfig() + val moduleType = ModuleType.fromPackageJson(packageJson) + astScrapper = JsAstScrapper( + parsedFile, + sourceFilePath, + Paths.get("$projectPath/$utbotDir"), + moduleType, + settings + ) val context = ServiceContext( utbotDir = utbotDir, projectPath = projectPath, filePathToInference = astScrapper.filesToInfer, parsedFile = parsedFile, settings = settings, + importsMap = astScrapper.importsMap, + packageJson = packageJson ) - context.packageJson = PackageJsonService( - sourceFilePath, - File(projectPath), - ).findClosestConfig() val paramNames = mutableMapOf>() val testSets = mutableListOf() val classNode = @@ -130,28 +141,40 @@ class JsTestGenerator( methods.forEach { funcNode -> makeTestsForMethod(classId, funcNode, classNode, context, testSets, paramNames) } - val importPrefix = makeImportPrefix() - val moduleType = ModuleType.fromPackageJson(context.packageJson) - val imports = listOf( + val imports = context.necessaryImports.makeImportsForCodegen(moduleType) + val codeGen = JsCodeGenerator( + classUnderTest = classId, + paramNames = paramNames, + imports = imports + ) + return codeGen.generateAsStringWithTestReport(testSets).generatedCode + } + + private fun Map.makeImportsForCodegen(moduleType: ModuleType): List { + val baseImports = listOf( JsImport( "*", - fileUnderTestAliases, - "./$importPrefix/${sourceFilePath.substringAfterLast("/")}", + "assert", + "assert", moduleType ), JsImport( "*", - "assert", - "assert", + fileUnderTestAliases, + "./${makeImportPrefix()}/${sourceFilePath.substringAfterLast("/")}", moduleType ) ) - val codeGen = JsCodeGenerator( - classUnderTest = classId, - paramNames = paramNames, - imports = imports - ) - return codeGen.generateAsStringWithTestReport(testSets).generatedCode + return baseImports + this.map { (key, value) -> + JsImport( + key, + key, + outputFilePath?.let { path -> + PathResolver.getRelativePath(File(path).parent, value.sourceFileName!!) + } ?: "", + moduleType + ) + } } private fun makeTestsForMethod( @@ -214,7 +237,7 @@ class JsTestGenerator( fuzzedValues: List ): UtExecutionResult { if (resultData.isError && resultData.rawString == "Timeout") return UtTimeoutException( - TimeoutException(" Timeout in generating test for ${ + TimeoutException("Timeout in generating test for ${ execId.parameters .zip(fuzzedValues) .joinToString( @@ -251,7 +274,7 @@ class JsTestGenerator( ) val collectedValues = mutableListOf>() // .location field gets us "jsFile:A:B", then we get A and B as ints - val funcLocation = funcNode.firstChild!!.location.substringAfter("jsFile:") + val funcLocation = funcNode.firstChild!!.location.substringAfter("${funcNode.sourceFileName}:") .split(":").map { it.toInt() } logger.info { "Function under test location according to parser is [${funcLocation[0]}, ${funcLocation[1]}]" } val instrService = InstrumentationService(context, funcLocation[0] to funcLocation[1]) @@ -392,9 +415,10 @@ class JsTestGenerator( private fun JsClassId.collectExportsRecursively(): List { return when { - this.isClass -> listOf(this.name) + (this.constructor?.parameters ?: emptyList()) + this.isClass && !astScrapper.importsMap.contains(this.name) -> + listOf(this.name) + (this.constructor?.parameters ?: emptyList()) .flatMap { it.collectExportsRecursively() } - + this is JsMultipleClassId -> this.classIds.flatMap { it.collectExportsRecursively() } this.isJsArray -> (this.elementClassId as? JsClassId)?.collectExportsRecursively() ?: emptyList() else -> emptyList() } diff --git a/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt index 8df0625040..6552527a5d 100644 --- a/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt +++ b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt @@ -5,10 +5,11 @@ import framework.api.js.JsEmptyClassId import framework.api.js.JsNullModel import framework.api.js.JsPrimitiveModel import framework.api.js.JsUndefinedModel +import framework.api.js.util.defaultJsValueModel import framework.api.js.util.jsErrorClassId -import framework.api.js.util.jsUndefinedClassId import fuzzer.JsIdProvider 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.UtExecutableCallModel import org.utbot.framework.plugin.api.UtModel @@ -20,10 +21,7 @@ class JsUtModelConstructor : UtModelConstructorInterface { @Suppress("NAME_SHADOWING") override fun construct(value: Any?, classId: ClassId): UtModel { val classId = classId as JsClassId - when (classId) { - jsUndefinedClassId -> return JsUndefinedModel(classId) - jsErrorClassId -> return UtModel(jsErrorClassId) - } + if (classId == jsErrorClassId) return UtModel(jsErrorClassId) return when (value) { null -> JsNullModel(classId) is Byte, @@ -35,7 +33,19 @@ class JsUtModelConstructor : UtModelConstructorInterface { is Double, is String, is Boolean -> JsPrimitiveModel(value) - + is List<*> -> { + UtArrayModel( + id = JsIdProvider.createId(), + classId = classId, + stores = buildMap { + putAll(value.indices.zip(value.map { + construct(it, classId) + })) + } as MutableMap, + length = value.size, + constModel = classId.defaultJsValueModel() + ) + } is Map<*, *> -> { constructObject(classId, value) } diff --git a/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt b/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt index ee5eb7858b..586846c793 100644 --- a/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt +++ b/utbot-js/src/main/kotlin/framework/api/js/JsApi.kt @@ -93,7 +93,7 @@ class JsConstructorId( get() = 0 } -class JsMultipleClassId(jsJoinedName: String) : JsClassId(jsJoinedName) +class JsMultipleClassId(val classIds: List) : JsClassId(classIds.joinToString(separator = "|") { it.name }) open class JsUtModel( override val classId: JsClassId 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 6ac73a0048..ba901ad050 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 @@ -44,13 +44,13 @@ fun JsClassId.defaultJsValueModel(): UtModel = when (this) { } val JsClassId.isJsBasic: Boolean - get() = this in jsBasic || this is JsMultipleClassId + get() = this in jsBasic || this.isJsStdStructure val JsClassId.isExportable: Boolean - get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure) + get() = !(this.isJsBasic || this == jsErrorClassId || this is JsMultipleClassId) val JsClassId.isClass: Boolean - get() = !(this.isJsBasic || this == jsErrorClassId || this.isJsStdStructure) + get() = !(this.isJsBasic || this == jsErrorClassId || this is JsMultipleClassId) val JsClassId.isUndefined: Boolean get() = this == jsUndefinedClassId 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 d4e71da534..7e8fedd52c 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,7 +1,5 @@ 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 @@ -50,7 +48,6 @@ import org.utbot.framework.codegen.renderer.CgAbstractRenderer import org.utbot.framework.codegen.renderer.CgPrinter import org.utbot.framework.codegen.renderer.CgPrinterImpl import org.utbot.framework.codegen.renderer.CgRendererContext -import org.utbot.framework.codegen.services.language.isLanguageKeyword import org.utbot.framework.codegen.tree.VisibilityModifier import org.utbot.framework.plugin.api.BuiltinMethodId import org.utbot.framework.plugin.api.ClassId @@ -257,10 +254,7 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP } override fun visit(element: CgConstructorCall) { - val importPrefix = "$fileUnderTestAliases.".takeIf { - (element.executableId.classId as JsClassId).isExportable - } ?: "" - print("new $importPrefix${element.executableId.classId.name}") + print("new ${element.executableId.classId.name}") print("(") element.arguments.renderSeparated() print(")") @@ -268,8 +262,16 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP 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\"") + ModuleType.COMMONJS -> { + if (name == "*") { + println("const $aliases = require (\"$path\")") + } else println("const {$aliases} = require(\"$path\")") + } + ModuleType.MODULE -> { + if (name == "*") { + println("import $name as $aliases from \"$path\"") + } else println("import {$name as $aliases} from \"$path\"") + } } } diff --git a/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt b/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt index c9953083d8..75887a91ce 100644 --- a/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt +++ b/utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt @@ -4,6 +4,7 @@ import framework.api.js.JsClassId import fuzzer.providers.ArrayValueProvider import fuzzer.providers.BoolValueProvider import fuzzer.providers.MapValueProvider +import fuzzer.providers.MultipleValueProvider import fuzzer.providers.NumberValueProvider import fuzzer.providers.ObjectValueProvider import fuzzer.providers.SetValueProvider @@ -19,6 +20,7 @@ fun defaultValueProviders() = listOf( StringValueProvider, MapValueProvider, SetValueProvider, + MultipleValueProvider, ObjectValueProvider(), ArrayValueProvider() ) diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/MultipleValueProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/MultipleValueProvider.kt new file mode 100644 index 0000000000..19a04d4195 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/MultipleValueProvider.kt @@ -0,0 +1,27 @@ +package fuzzer.providers + +import framework.api.js.JsClassId +import framework.api.js.JsMultipleClassId +import fuzzer.JsMethodDescription +import fuzzer.defaultValueProviders +import org.utbot.framework.plugin.api.UtModel +import org.utbot.fuzzing.Seed +import org.utbot.fuzzing.ValueProvider + +object MultipleValueProvider : ValueProvider { + + override fun accept(type: JsClassId): Boolean { + return type is JsMultipleClassId + } + + override fun generate(description: JsMethodDescription, type: JsClassId): Sequence> = + sequence { + for (classId in (type as JsMultipleClassId).classIds) { + for (provider in defaultValueProviders()) { + if (provider.accept(classId)) { + yieldAll(provider.generate(description, classId)) + } + } + } + } +} diff --git a/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt b/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt index a22b232cf2..ec31f3a9d7 100644 --- a/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt +++ b/utbot-js/src/main/kotlin/parser/JsAstScrapper.kt @@ -4,9 +4,12 @@ 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 framework.codegen.ModuleType import java.io.File +import java.nio.file.Path import java.nio.file.Paths import mu.KotlinLogging +import org.json.JSONObject import parser.JsParserUtils.getAbstractFunctionName import parser.JsParserUtils.getClassMethods import parser.JsParserUtils.getImportSpecAliases @@ -14,7 +17,15 @@ import parser.JsParserUtils.getImportSpecName import parser.JsParserUtils.getModuleImportSpecsAsList import parser.JsParserUtils.getModuleImportText import parser.JsParserUtils.getRequireImportText +import parser.JsParserUtils.isAnyVariableDecl import parser.JsParserUtils.isRequireImport +import parser.visitors.IAstVisitor +import parser.visitors.JsClassAstVisitor +import parser.visitors.JsFunctionAstVisitor +import parser.visitors.JsVariableAstVisitor +import settings.JsDynamicSettings +import utils.JsCmdExec +import utils.PathResolver import kotlin.io.path.pathString private val logger = KotlinLogging.logger {} @@ -22,10 +33,13 @@ private val logger = KotlinLogging.logger {} class JsAstScrapper( private val parsedFile: Node, private val basePath: String, + private val tempUtbotPath: Path, + private val moduleType: ModuleType, + private val settings: JsDynamicSettings ) { // Used not to parse the same file multiple times. - private val _parsedFilesCache = mutableMapOf() + private val _parsedFilesCache = mutableMapOf(Paths.get(basePath) to parsedFile) private val _filesToInfer: MutableList = mutableListOf(basePath) val filesToInfer: List get() = _filesToInfer.toList() @@ -35,14 +49,22 @@ class JsAstScrapper( 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) + val processedFiles = mutableSetOf() + fun Node.collectImportsRec(): List> { + processedFiles.add(Paths.get(this.sourceFileName!!)) + val vis = Visitor() + vis.accept(this) + return vis.importNodes.flatMap { node -> + val temp = node.importedNodes() + temp.toList() + temp.flatMap { entry -> + val path = Paths.get(entry.value.sourceFileName!!) + if (!processedFiles.contains(path)) { + path.toFile().parseIfNecessary().collectImportsRec() + } else emptyList() + } + } } - this.putAll(res) + this.putAll(parsedFile.collectImportsRec().toMap()) this.toMap() } } @@ -53,7 +75,9 @@ class JsAstScrapper( functionVisitor.accept(file) return try { functionVisitor.targetFunctionNode - } catch (e: Exception) { null } + } catch (e: Exception) { + null + } } fun findClass(key: String, file: Node): Node? { @@ -62,7 +86,9 @@ class JsAstScrapper( classVisitor.accept(file) return try { classVisitor.targetClassNode - } catch (e: Exception) { null } + } catch (e: Exception) { + null + } } fun findMethod(classKey: String, methodKey: String, file: Node): Node? { @@ -70,25 +96,87 @@ class JsAstScrapper( return classNode?.getClassMethods()?.find { it.getAbstractFunctionName() == methodKey } } + fun findVariable(key: String, file: Node): Node? { + if (_importsMap[key]?.isAnyVariableDecl() == true) return _importsMap[key] + val variableVisitor = JsVariableAstVisitor(key) + variableVisitor.accept(file) + return try { + variableVisitor.targetVariableNode + } catch (e: Exception) { + null + } + } + private fun File.parseIfNecessary(): Node = - _parsedFilesCache.getOrPut(this.path) { + _parsedFilesCache.getOrPut(this.toPath()) { _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.isRequireImport() -> this.processRequireImport() this.isImport -> this.processModuleImport() else -> emptyMap() } } + @Suppress("unchecked_cast") + private fun getExportedKeys(importPath: String): List { + val pathToTempFile = Paths.get(tempUtbotPath.pathString + "/exp_temp.js") + val text = buildString { + when (moduleType) { + ModuleType.COMMONJS -> { + appendLine("const temp = require(\"$importPath\")") + appendLine("const fs = require(\"fs\")") + } + + else -> { + appendLine("import * as temp from \"$importPath\"") + appendLine("import * as fs from \"fs\"") + } + } + appendLine("fs.writeFileSync(\"exp_temp.json\", JSON.stringify({files: Object.keys(temp)}))") + } + pathToTempFile.toFile().writeText(text) + JsCmdExec.runCommand( + cmd = arrayOf("\"${settings.pathToNode}\"", pathToTempFile.pathString), + dir = pathToTempFile.parent.pathString, + shouldWait = true, + timeout = settings.timeout, + ) + val pathToJson = pathToTempFile.pathString.replace(".js", ".json") + return JSONObject(File(pathToJson).readText()).getJSONArray("files").toList() as List + } + + private fun Node.processRequireImport(): Map { + try { + val pathToFile = makePathFromImport(this.getRequireImportText()) ?: return emptyMap() + val pFile = File(pathToFile).parseIfNecessary() + val objPattern = NodeUtil.findPreorder(this.parent, { it.isObjectPattern }, { true }) + return when { + objPattern != null -> { + buildList { + var currNode: Node? = objPattern.firstChild!! + while (currNode != null) { + add(currNode.string) + currNode = currNode.next + } + }.associateWith { key -> pFile.findEntityInFile(key) } + } + else -> { + val importPath = PathResolver.getRelativePath(tempUtbotPath.pathString, pathToFile) + getExportedKeys(importPath).associateWith { key -> + pFile.findEntityInFile(key) + } + } + } + } catch (e: ClassNotFoundException) { + logger.error { e.toString() } + return emptyMap() + } + } + private fun Node.processModuleImport(): Map { try { val pathToFile = makePathFromImport(this.getModuleImportText()) ?: return emptyMap() @@ -101,9 +189,13 @@ class JsAstScrapper( aliases to pFile.findEntityInFile(realName) } } + NodeUtil.findPreorder(this, { it.isImportStar }, { true }) != null -> { val aliases = this.getImportSpecAliases() - mapOf(aliases to pFile) + val importPath = PathResolver.getRelativePath(tempUtbotPath.pathString, pathToFile) + getExportedKeys(importPath).associateWith { key -> + pFile.findEntityInFile(key) + } } // For example: import foo from "bar" else -> { @@ -111,7 +203,7 @@ class JsAstScrapper( mapOf(realName to pFile.findEntityInFile(realName)) } } - } catch (e: Exception) { + } catch (e: ClassNotFoundException) { logger.error { e.toString() } return emptyMap() } @@ -125,25 +217,25 @@ class JsAstScrapper( } private fun Node.findEntityInFile(key: String?): Node { - return key?.let { k -> + return key?.let { k -> findClass(k, this) ?: findFunction(k, this) + ?: findVariable(k, this) ?: throw ClassNotFoundException("Could not locate entity $k in ${this.sourceFileName}") } ?: this } - private class Visitor: IAstVisitor { + 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 -// } + NodeUtil.visitPreOrder(rootNode) { node -> + if (node.isImport || node.isRequireImport()) _importNodes += node + } } } } diff --git a/utbot-js/src/main/kotlin/parser/JsParserUtils.kt b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt index e1d1df2385..3b3a3d2f49 100644 --- a/utbot-js/src/main/kotlin/parser/JsParserUtils.kt +++ b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt @@ -6,14 +6,15 @@ import com.google.javascript.jscomp.SourceFile import com.google.javascript.rhino.Node import fuzzer.JsFuzzedContext import parser.JsParserUtils.getMethodName +import parser.visitors.JsClassAstVisitor // TODO: make methods more safe by checking the Node method is called on. // Used for .children() calls. @Suppress("DEPRECATION") object JsParserUtils { - fun runParser(fileText: String): Node = - Compiler().parse(SourceFile.fromCode("jsFile", fileText)) + fun runParser(fileText: String, filePath: String): Node = + Compiler().parse(SourceFile.fromCode(filePath, fileText)) // TODO SEVERE: function only works in the same file scope. Add search in exports. fun searchForClassDecl(className: String?, parsedFile: Node, strict: Boolean = false): Node? { @@ -206,4 +207,27 @@ object JsParserUtils { */ fun Node.getImportSpecAliases(): String = this.firstChild!!.next!!.string + /** + * Checks if node is any kind of variable declaration. + */ + fun Node.isAnyVariableDecl(): Boolean = + this.isVar || this.isConst || this.isLet + + /** + * Called upon any variable declaration node. + * + * Returns variable name as [String]. + */ + fun Node.getVariableName(): String? = try { + this.firstChild!!.string + } catch (_: Exception) { + null + } + + /** + * Called upon any variable declaration node. + * + * Returns variable initializer as [Node] + */ + fun Node.getVariableValue(): Node = this.firstChild!!.firstChild!! } diff --git a/utbot-js/src/main/kotlin/parser/IAstVisitor.kt b/utbot-js/src/main/kotlin/parser/visitors/IAstVisitor.kt similarity index 79% rename from utbot-js/src/main/kotlin/parser/IAstVisitor.kt rename to utbot-js/src/main/kotlin/parser/visitors/IAstVisitor.kt index 1d8bcf64a2..b819c4efaa 100644 --- a/utbot-js/src/main/kotlin/parser/IAstVisitor.kt +++ b/utbot-js/src/main/kotlin/parser/visitors/IAstVisitor.kt @@ -1,8 +1,8 @@ -package parser +package parser.visitors import com.google.javascript.rhino.Node interface IAstVisitor { fun accept(rootNode: Node) -} \ No newline at end of file +} diff --git a/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt b/utbot-js/src/main/kotlin/parser/visitors/JsClassAstVisitor.kt similarity index 96% rename from utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt rename to utbot-js/src/main/kotlin/parser/visitors/JsClassAstVisitor.kt index 925d9a22f1..6d929eef6e 100644 --- a/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt +++ b/utbot-js/src/main/kotlin/parser/visitors/JsClassAstVisitor.kt @@ -1,4 +1,4 @@ -package parser +package parser.visitors import com.google.javascript.jscomp.NodeUtil import com.google.javascript.rhino.Node @@ -23,4 +23,4 @@ class JsClassAstVisitor( } } } -} \ No newline at end of file +} diff --git a/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/visitors/JsFunctionAstVisitor.kt similarity index 98% rename from utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt rename to utbot-js/src/main/kotlin/parser/visitors/JsFunctionAstVisitor.kt index 9b50843575..a067e58ee7 100644 --- a/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt +++ b/utbot-js/src/main/kotlin/parser/visitors/JsFunctionAstVisitor.kt @@ -1,4 +1,4 @@ -package parser +package parser.visitors import com.google.javascript.jscomp.NodeUtil import com.google.javascript.rhino.Node @@ -38,4 +38,4 @@ class JsFunctionAstVisitor( } } } -} \ No newline at end of file +} diff --git a/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt b/utbot-js/src/main/kotlin/parser/visitors/JsFuzzerAstVisitor.kt similarity index 98% rename from utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt rename to utbot-js/src/main/kotlin/parser/visitors/JsFuzzerAstVisitor.kt index 463edcfb5b..6172ecf29b 100644 --- a/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt +++ b/utbot-js/src/main/kotlin/parser/visitors/JsFuzzerAstVisitor.kt @@ -1,4 +1,4 @@ -package parser +package parser.visitors import com.google.javascript.jscomp.NodeUtil diff --git a/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/visitors/JsToplevelFunctionAstVisitor.kt similarity index 94% rename from utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt rename to utbot-js/src/main/kotlin/parser/visitors/JsToplevelFunctionAstVisitor.kt index 92e32d3b0e..f0cb287764 100644 --- a/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt +++ b/utbot-js/src/main/kotlin/parser/visitors/JsToplevelFunctionAstVisitor.kt @@ -1,4 +1,4 @@ -package parser +package parser.visitors import com.google.javascript.jscomp.NodeUtil import com.google.javascript.rhino.Node @@ -15,4 +15,4 @@ class JsToplevelFunctionAstVisitor : IAstVisitor { } } } -} \ No newline at end of file +} diff --git a/utbot-js/src/main/kotlin/parser/visitors/JsVariableAstVisitor.kt b/utbot-js/src/main/kotlin/parser/visitors/JsVariableAstVisitor.kt new file mode 100644 index 0000000000..73d9658d59 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/visitors/JsVariableAstVisitor.kt @@ -0,0 +1,21 @@ +package parser.visitors + +import com.google.javascript.jscomp.NodeUtil +import com.google.javascript.rhino.Node +import parser.JsParserUtils.getVariableName +import parser.JsParserUtils.isAnyVariableDecl + +class JsVariableAstVisitor( + private val target: String +): IAstVisitor { + + lateinit var targetVariableNode: Node + + override fun accept(rootNode: Node) = + NodeUtil.visitPreOrder(rootNode) { + if (it.isAnyVariableDecl() && it.getVariableName() == target) { + targetVariableNode = it + return@visitPreOrder + } + } +} diff --git a/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt b/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt index 7d1aa55c01..d1d31e2e10 100644 --- a/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt +++ b/utbot-js/src/main/kotlin/providers/imports/ModuleImportsProvider.kt @@ -3,6 +3,7 @@ package providers.imports import service.ContextOwner import service.ServiceContext import settings.JsTestGenerationSettings.fileUnderTestAliases +import utils.PathResolver class ModuleImportsProvider(context: ServiceContext) : IImportsProvider, ContextOwner by context { @@ -17,6 +18,11 @@ class ModuleImportsProvider(context: ServiceContext) : IImportsProvider, Context override val tempFileImports: String = buildString { val importFileUnderTest = "./instr/${filePathToInference.first().substringAfterLast("/")}" appendLine("import * as $fileUnderTestAliases from \"$importFileUnderTest\"") + val currDir = "${projectPath}/${utbotDir}" + for ((key, value) in necessaryImports) { + val importPath = PathResolver.getRelativePath(currDir, value.sourceFileName!!) + appendLine("import {$key} from \"$importPath\"") + } 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 index d653b2b047..c62c9c13d5 100644 --- a/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt +++ b/utbot-js/src/main/kotlin/providers/imports/RequireImportsProvider.kt @@ -3,6 +3,7 @@ package providers.imports import service.ContextOwner import service.ServiceContext import settings.JsTestGenerationSettings.fileUnderTestAliases +import utils.PathResolver class RequireImportsProvider(context: ServiceContext) : IImportsProvider, ContextOwner by context { @@ -17,7 +18,12 @@ class RequireImportsProvider(context: ServiceContext) : IImportsProvider, Contex override val tempFileImports: String = buildString { val importFileUnderTest = "instr/${filePathToInference.first().substringAfterLast("/")}" appendLine("const $fileUnderTestAliases = require(\"./$importFileUnderTest\")") + val currDir = "${projectPath}/${utbotDir}" + for ((key, value) in necessaryImports) { + val importPath = PathResolver.getRelativePath(currDir, value.sourceFileName!!) + appendLine("const {$key} = require(\"$importPath\")") + } 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 e36dd0e41d..70b8afdf6d 100644 --- a/utbot-js/src/main/kotlin/service/InstrumentationService.kt +++ b/utbot-js/src/main/kotlin/service/InstrumentationService.kt @@ -4,7 +4,7 @@ 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.visitors.JsFunctionAstVisitor import parser.JsParserUtils.getAnyValue import parser.JsParserUtils.getRequireImportText import parser.JsParserUtils.isRequireImport @@ -142,7 +142,7 @@ class InstrumentationService(context: ServiceContext, private val funcDeclOffset timeout = settings.timeout, ) val instrumentedFileText = File(instrumentedFilePath).readText() - parsedInstrFile = runParser(instrumentedFileText) + parsedInstrFile = runParser(instrumentedFileText, instrumentedFilePath) val covFunRegex = Regex("function (cov_.*)\\(\\).*") val funName = covFunRegex.find(instrumentedFileText.takeWhile { it != '{' })?.groups?.get(1)?.value ?: throw IllegalStateException("") @@ -155,7 +155,7 @@ class InstrumentationService(context: ServiceContext, private val funcDeclOffset private fun File.writeTextAndUpdate(newText: String) { this.writeText(newText) - parsedInstrFile = runParser(File(instrumentedFilePath).readText()) + parsedInstrFile = runParser(File(instrumentedFilePath).readText(), instrumentedFilePath) } private fun fixImportsInInstrumentedFile(): String { diff --git a/utbot-js/src/main/kotlin/service/ServiceContext.kt b/utbot-js/src/main/kotlin/service/ServiceContext.kt index d30ccf1fea..1708e30bb0 100644 --- a/utbot-js/src/main/kotlin/service/ServiceContext.kt +++ b/utbot-js/src/main/kotlin/service/ServiceContext.kt @@ -9,7 +9,9 @@ class ServiceContext( override val filePathToInference: List, override val parsedFile: Node, override val settings: JsDynamicSettings, - override var packageJson: PackageJson = PackageJson.defaultConfig + override var packageJson: PackageJson = PackageJson.defaultConfig, + override val importsMap: Map, + override val necessaryImports: MutableMap = mutableMapOf() ) : ContextOwner interface ContextOwner { @@ -19,4 +21,6 @@ interface ContextOwner { val parsedFile: Node val settings: JsDynamicSettings var packageJson: PackageJson + val importsMap: Map + val necessaryImports: MutableMap } diff --git a/utbot-js/src/main/kotlin/service/TernService.kt b/utbot-js/src/main/kotlin/service/TernService.kt index faf526d0d1..6b3a76e80a 100644 --- a/utbot-js/src/main/kotlin/service/TernService.kt +++ b/utbot-js/src/main/kotlin/service/TernService.kt @@ -20,7 +20,7 @@ import java.io.File /** * Installs and sets up scripts for running Tern.js type guesser. */ -class TernService(context: ServiceContext) : ContextOwner by context { +class TernService(val context: ServiceContext) : ContextOwner by context { private val importProvider = IImportsProvider.providerByPackageJson(packageJson, context) @@ -104,7 +104,7 @@ test(["${filePathToInference.joinToString(separator = "\", \"")}"]) val parametersRegex = Regex("fn[(](.+)[)]") return parametersRegex.find(line)?.groups?.get(1)?.let { matchResult -> val value = matchResult.value - val paramGroupList = Regex("(\\w+:\\[\\w+(,\\w+)*]|\\w+:\\w+)|\\w+:\\?").findAll(value).toList() + val paramGroupList = Regex("(\\w+:\\[[\\w|]+(,[\\w|]+)*]|\\w+:[\\w|]+)|\\w+:\\?").findAll(value).toList() paramGroupList.map { paramGroup -> val paramReg = Regex("\\w*:(.*)") try { @@ -170,12 +170,14 @@ test(["${filePathToInference.joinToString(separator = "\", \"")}"]) ) } - name.contains('|') -> JsMultipleClassId(name) + name.contains('|') -> { + JsMultipleClassId(name.split("|").map { makeClassId(it) }) + } else -> JsClassId(name) } return try { - val classNode = JsParserUtils.searchForClassDecl( + val classNode = importsMap[name] ?: JsParserUtils.searchForClassDecl( className = name, parsedFile = parsedFile, strict = true, diff --git a/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt b/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt index 7d027d1d91..7f4dc2de17 100644 --- a/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt +++ b/utbot-js/src/main/kotlin/service/coverage/CoverageServiceProvider.kt @@ -1,11 +1,10 @@ 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 java.util.regex.Pattern import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtExecutableCallModel @@ -20,7 +19,6 @@ import settings.JsTestGenerationSettings import settings.JsTestGenerationSettings.tempFileName import utils.data.CoverageData import utils.data.ResultData -import java.util.regex.Pattern class CoverageServiceProvider( private val context: ServiceContext, @@ -216,10 +214,7 @@ fs.writeFileSync("$resFilePath$index.json", JSON.stringify(json$index)) } private fun UtAssembleModel.toParamString(): String { - val importPrefix = "new ${JsTestGenerationSettings.fileUnderTestAliases}.".takeIf { - (classId as JsClassId).isExportable - } ?: "new " - val callConstructorString = importPrefix + classId.name + val callConstructorString = "new " + classId.name val paramsString = instantiationCall.params.joinToString( prefix = "(", postfix = ")", diff --git a/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt index 54149f9202..2f0507268d 100644 --- a/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt +++ b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt @@ -16,7 +16,9 @@ fun JsClassId.constructClass( classNode: Node? = null, functions: List = emptyList() ): JsClassId { - val className = classNode?.getClassName() + val className = classNode?.getClassName()?.also { + ternService.context.necessaryImports[it] = classNode + } val methods = constructMethods(classNode, ternService, className, functions) val constructor = classNode?.let { diff --git a/utbot-js/src/main/kotlin/utils/ValueUtil.kt b/utbot-js/src/main/kotlin/utils/ValueUtil.kt index 50862cd4b4..1c32a1c938 100644 --- a/utbot-js/src/main/kotlin/utils/ValueUtil.kt +++ b/utbot-js/src/main/kotlin/utils/ValueUtil.kt @@ -1,7 +1,5 @@ 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 @@ -9,6 +7,8 @@ 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 { @@ -17,8 +17,11 @@ fun ResultData.toJsAny(returnType: JsClassId = jsUndefinedClassId): Pair toBoolean() to jsBooleanClassId this == "null" || this == "undefined" -> null to jsUndefinedClassId + Regex("\\[.*]").matches(this) && returnType.name == "object" -> + makeArray(this) to JsClassId("array", elementClassId = jsUndefinedClassId) this@toJsAny.isError -> this to jsErrorClassId - returnType == jsStringClassId -> this.replace("\"", "") to jsStringClassId + returnType == jsStringClassId || this@toJsAny.type == jsStringClassId.name -> + this.replace("\"", "") to jsStringClassId else -> { if (contains('.')) { (toDoubleOrNull() ?: toBigDecimal()) to jsDoubleClassId @@ -27,7 +30,9 @@ fun ResultData.toJsAny(returnType: JsClassId = jsUndefinedClassId): Pair? { null } } + +private fun makeArray(arrString: String): List { + val strValues = arrString.replace(Regex("[\\[\\]]"), "").split(",") + return strValues.map { ResultData(it, index = 0).toJsAny(jsUndefinedClassId).first } +}