Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new import/export system for JavaScript #2188

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion utbot-js/samples/commonIfStatement.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function foo(a,b) {
function foo(a, b) {
if (a > 10) {
return a * b
} else {
Expand Down
2 changes: 1 addition & 1 deletion utbot-js/samples/mapStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ function simpleMap(map, compareValue) {

const map1 = new Map()
map1.set("b", 3.0)
simpleMap(map1, 5)
simpleMap(map1, 5)
7 changes: 7 additions & 0 deletions utbot-js/samples/multi_file_tests/basic_import/basicImport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {ObjectParameter} = require("./toImport.js")

function test(obj) {
return obj.performAction(5)
}

test(new ObjectParameter(5))
12 changes: 12 additions & 0 deletions utbot-js/samples/multi_file_tests/basic_import/toImport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class ObjectParameter {

constructor(a) {
this.first = a
}

performAction(value) {
return 2 * value
}
}

exports.ObjectParameter = ObjectParameter
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {glob} = require("./toImport.js")

function test(obj) {
return obj.performAction(5)
}

test(glob)
8 changes: 8 additions & 0 deletions utbot-js/samples/multi_file_tests/chain_imports/temp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Some {

constructor(b) {
this.b = b
}
}

exports.Some = Some
18 changes: 18 additions & 0 deletions utbot-js/samples/multi_file_tests/chain_imports/toImport.js
Original file line number Diff line number Diff line change
@@ -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
8 changes: 2 additions & 6 deletions utbot-js/samples/scenarioMultyClassNoTopLevel.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
class Na {
class Double {
constructor(num) {
this.num = num
}

double() {
return this.num * 2
}

static test(a, b) {
return a + 2 * b
}
}

class Kek {
class Functions {
foo(a, b) {
return a + b
}
Expand Down
3 changes: 1 addition & 2 deletions utbot-js/samples/scenarioThrowError.js
Original file line number Diff line number Diff line change
@@ -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
}

}
2 changes: 1 addition & 1 deletion utbot-js/samples/setStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ function setTest(set, checkValue) {
let s = new Set()
s.add(5)
s.add(6)
setTest(s, 4)
setTest(s, 4)
74 changes: 49 additions & 25 deletions utbot-js/src/main/kotlin/api/JsTestGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -36,15 +37,15 @@ 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
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
Expand All @@ -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
Expand Down Expand Up @@ -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<ExecutableId, List<String>>()
val testSets = mutableListOf<CgMethodTestSet>()
val classNode =
Expand All @@ -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<String, Node>.makeImportsForCodegen(moduleType: ModuleType): List<JsImport> {
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(
Expand Down Expand Up @@ -214,7 +237,7 @@ class JsTestGenerator(
fuzzedValues: List<UtModel>
): 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(
Expand Down Expand Up @@ -251,7 +274,7 @@ class JsTestGenerator(
)
val collectedValues = mutableListOf<List<UtModel>>()
// .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])
Expand Down Expand Up @@ -392,9 +415,10 @@ class JsTestGenerator(

private fun JsClassId.collectExportsRecursively(): List<String> {
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()
}
Expand Down
22 changes: 16 additions & 6 deletions utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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<Int, UtModel>,
length = value.size,
constModel = classId.defaultJsValueModel()
)
}
is Map<*, *> -> {
constructObject(classId, value)
}
Expand Down
2 changes: 1 addition & 1 deletion utbot-js/src/main/kotlin/framework/api/js/JsApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class JsConstructorId(
get() = 0
}

class JsMultipleClassId(jsJoinedName: String) : JsClassId(jsJoinedName)
class JsMultipleClassId(val classIds: List<JsClassId>) : JsClassId(classIds.joinToString(separator = "|") { it.name })

open class JsUtModel(
override val classId: JsClassId
Expand Down
6 changes: 3 additions & 3 deletions utbot-js/src/main/kotlin/framework/api/js/util/JsIdUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -257,19 +254,24 @@ 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(")")
}

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\"")
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions utbot-js/src/main/kotlin/fuzzer/JsFuzzing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,6 +20,7 @@ fun defaultValueProviders() = listOf(
StringValueProvider,
MapValueProvider,
SetValueProvider,
MultipleValueProvider,
ObjectValueProvider(),
ArrayValueProvider()
)
Expand Down
Loading