From 0c74376cc4308b52775a5bc9ee933c0ec3c1ce14 Mon Sep 17 00:00:00 2001 From: Ilya Goncharov Date: Wed, 8 Dec 2021 08:22:53 +0000 Subject: [PATCH] rra/ilgonmic/after-test-promise [JS] Node downloading for js ir integration kotlin test [JS] Fix API of Promise [JS IR] Promise symbol as lazy2 [JS] Support legacy compiler aftertest with promises [JS IR] Generate finally for promised tests [JS] Setup it tests for JS IR kotlin-test Merge-request: KT-MR-5168 ^KT-49738 fixed --- build.gradle.kts | 1 + .../backend/common/CompilationException.kt | 4 + .../kotlin/ir/backend/js/JsIntrinsics.kt | 3 + .../ir/backend/js/lower/TestGenerator.kt | 56 +++++++- .../js/translate/test/JSTestGenerator.kt | 39 +++++- libraries/kotlin.test/js-ir/it/.gitignore | 1 + .../kotlin.test/js-ir/it/build.gradle.kts | 127 ++++++++++++++++++ libraries/kotlin.test/js-ir/it/package.json | 20 +++ .../js/it/src/test/kotlin/MainTest.kt | 10 +- libraries/stdlib/api/js-v1/kotlin.js.kt | 2 + libraries/stdlib/api/js/kotlin.js.kt | 2 + libraries/stdlib/js/src/kotlin/promise.kt | 2 + settings.gradle | 2 + 13 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 libraries/kotlin.test/js-ir/it/.gitignore create mode 100644 libraries/kotlin.test/js-ir/it/build.gradle.kts create mode 100644 libraries/kotlin.test/js-ir/it/package.json diff --git a/build.gradle.kts b/build.gradle.kts index 64a5f1b7249..5bd5a7d761d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -672,6 +672,7 @@ tasks { ":kotlin-stdlib-js-ir", ":kotlin-test:kotlin-test-js-ir".takeIf { !kotlinBuildProperties.isInJpsBuildIdeaSync }, ":kotlin-test:kotlin-test-js:kotlin-test-js-it".takeIf { !kotlinBuildProperties.isInJpsBuildIdeaSync }, + ":kotlin-test:kotlin-test-js-ir:kotlin-test-js-ir-it".takeIf { !kotlinBuildProperties.isInJpsBuildIdeaSync }, ":kotlinx-metadata-jvm", ":tools:binary-compatibility-validator" )).forEach { diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/CompilationException.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/CompilationException.kt index 7c38b81773f..bbdcf0807a3 100644 --- a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/CompilationException.kt +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/CompilationException.kt @@ -34,6 +34,8 @@ class CompilationException( val irStartOffset = irStartOffset ?: return UNDEFINED_OFFSET + if (irStartOffset == UNDEFINED_OFFSET) return UNDEFINED_OFFSET + val lineNumber = file?.fileEntry?.getLineNumber(irStartOffset) ?: return UNDEFINED_OFFSET @@ -45,6 +47,8 @@ class CompilationException( val irStartOffset = irStartOffset ?: return UNDEFINED_OFFSET + if (irStartOffset == UNDEFINED_OFFSET) return UNDEFINED_OFFSET + val columnNumber = file?.fileEntry?.getColumnNumber(irStartOffset) ?: return UNDEFINED_OFFSET diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt index 54d8180b30b..230b934ecf8 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt @@ -178,6 +178,9 @@ class JsIntrinsics(private val irBuiltIns: IrBuiltIns, val context: JsIrBackendC val jsNumberRangeToLong = getInternalFunction("numberRangeToLong") val longClassSymbol = getInternalClassWithoutPackage("kotlin.Long") + val promiseClassSymbol: IrClassSymbol by context.lazy2 { + getInternalClassWithoutPackage("kotlin.js.Promise") + } val longToDouble = context.symbolTable.referenceSimpleFunction( context.getClass(FqName("kotlin.Long")).unsubstitutedMemberScope.findSingleFunction( diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt index 52f74a7d0b7..dd5aadca0b8 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt @@ -22,6 +22,7 @@ import org.jetbrains.kotlin.ir.expressions.IrBlockBody import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.name.FqName @@ -168,15 +169,60 @@ class TestGenerator(val context: JsCommonBackendContext, val groupByPackage: Boo if (afterFuns.isEmpty()) { body.statements += returnStatement - } else { - body.statements += JsIrBuilder.buildTry(context.irBuiltIns.unitType).apply { - tryResult = returnStatement - finallyExpression = JsIrBuilder.buildComposite(context.irBuiltIns.unitType).apply { - statements += afterFuns.map { + return + } + + if (context is JsIrBackendContext && (testFun.returnType as? IrSimpleType)?.classifier == context.intrinsics.promiseClassSymbol) { + val finally = context.intrinsics.promiseClassSymbol.owner.declarations + .filterIsInstance() + .first { + it.name.asString() == "finally" + } + + val refType = IrSimpleTypeImpl(context.ir.symbols.functionN(0), false, emptyList(), emptyList()) + + val afterFunction = context.irFactory.buildFun { + this.name = Name.identifier("${irClass.name.asString()} after test fun") + this.returnType = context.irBuiltIns.unitType + this.origin = JsIrBuilder.SYNTHESIZED_DECLARATION + }.apply { + parent = fn + this.body = context.irFactory.createBlockBody( + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + afterFuns.map { JsIrBuilder.buildCall(it.symbol).apply { dispatchReceiver = JsIrBuilder.buildGetValue(classVal.symbol) } } + ) + } + + val finallyLambda = JsIrBuilder.buildFunctionExpression(refType, afterFunction) + + val returnValue = JsIrBuilder.buildCall( + finally.symbol + ).apply { + this.dispatchReceiver = returnStatement.value + putValueArgument(0, finallyLambda) + } + + body.statements += JsIrBuilder.buildReturn( + fn.symbol, + returnValue, + fn.returnType + ) + + return + } + + body.statements += JsIrBuilder.buildTry(context.irBuiltIns.unitType).apply { + tryResult = returnStatement + finallyExpression = JsIrBuilder.buildComposite(context.irBuiltIns.unitType).apply { + statements += afterFuns.map { + JsIrBuilder.buildCall(it.symbol).apply { + dispatchReceiver = JsIrBuilder.buildGetValue(classVal.symbol) + } } } } diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/test/JSTestGenerator.kt b/js/js.translator/src/org/jetbrains/kotlin/js/translate/test/JSTestGenerator.kt index 0494aeb0f7c..3c330e874c6 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/test/JSTestGenerator.kt +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/test/JSTestGenerator.kt @@ -21,6 +21,7 @@ import org.jetbrains.kotlin.js.backend.ast.* import org.jetbrains.kotlin.js.translate.callTranslator.CallTranslator import org.jetbrains.kotlin.js.translate.context.TranslationContext import org.jetbrains.kotlin.js.translate.reference.ReferenceTranslator +import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqNameUnsafe import org.jetbrains.kotlin.name.Name @@ -121,14 +122,37 @@ class JSTestGenerator(val context: TranslationContext) { if (afterDescriptors.isEmpty()) { functionToTest.body.statements += JsReturn(functionDescriptor.buildCall()) + return functionToTest } - else { - functionToTest.body.statements += JsTry( - JsBlock(JsReturn(functionDescriptor.buildCall())), - listOf(), - JsBlock(afterDescriptors.map { it.buildCall().makeStmt() })) + + val declarationDescriptor: ClassifierDescriptor? = functionDescriptor.returnType?.constructor?.declarationDescriptor + val promiseClass = findPromise() + if (declarationDescriptor == promiseClass) { + val call = functionDescriptor.buildCall() + val finallyDescriptor = DescriptorUtils.getFunctionByName(promiseClass.unsubstitutedMemberScope, Name.identifier("finally")) + functionToTest.body.statements += JsReturn( + CallTranslator.buildCall( + context, + finallyDescriptor, + listOf( + JsFunction( + scope, + JsBlock(afterDescriptors.map { it.buildCall().makeStmt() }), + "function for after funs test" + ) + ), + call + ) + ) + return functionToTest } + functionToTest.body.statements += JsTry( + JsBlock(JsReturn(functionDescriptor.buildCall())), + listOf(), + JsBlock(afterDescriptors.map { it.buildCall().makeStmt() }) + ) + return functionToTest } @@ -142,6 +166,9 @@ class JSTestGenerator(val context: TranslationContext) { } } + fun findPromise(): ClassDescriptor = + context.currentModule.findClassAcrossModuleDependencies(ClassId.topLevel(promiseFqName))!! + private val suiteRef: JsExpression by lazy { findFunction("suite") } private val testRef: JsExpression by lazy { findFunction("test") } @@ -164,6 +191,8 @@ class JSTestGenerator(val context: TranslationContext) { private val FunctionDescriptor.isAfter get() = annotationFinder("AfterTest", "kotlin.test") + private val promiseFqName = FqName("kotlin.js.Promise") + private fun DeclarationDescriptor.annotationFinder(shortName: String, vararg packages: String) = packages.any { packageName -> annotations.hasAnnotation(FqName("$packageName.$shortName")) } diff --git a/libraries/kotlin.test/js-ir/it/.gitignore b/libraries/kotlin.test/js-ir/it/.gitignore new file mode 100644 index 00000000000..3c3629e647f --- /dev/null +++ b/libraries/kotlin.test/js-ir/it/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/libraries/kotlin.test/js-ir/it/build.gradle.kts b/libraries/kotlin.test/js-ir/it/build.gradle.kts new file mode 100644 index 00000000000..985b589e5ac --- /dev/null +++ b/libraries/kotlin.test/js-ir/it/build.gradle.kts @@ -0,0 +1,127 @@ +import com.moowork.gradle.node.npm.NpmTask +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages +import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrLink +import java.io.FileOutputStream + +plugins { + kotlin("js") + id("com.github.node-gradle.node") version "2.2.0" +} + +description = "Kotlin-test integration tests for JS IR" + +node { + version = "16.13.0" + download = true +} + +val jsMainSources by task { + from("$rootDir/libraries/kotlin.test/js/it/src") + into("$buildDir/jsMainSources") +} + +val jsSources by task { + from("$rootDir/libraries/kotlin.test/js/it/js") + into("$buildDir/jsSources") +} + +val ignoreTestFailures by extra(project.kotlinBuildProperties.ignoreTestFailures) + +kotlin { + js(IR) { + nodejs { + testTask { + enabled = false + } + } + } + + sourceSets { + val test by getting { + kotlin.srcDir(jsMainSources.get().destinationDir) + } + } +} + +val nodeModules by configurations.registering { + extendsFrom(configurations["api"]) + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, KotlinUsages.KOTLIN_RUNTIME)) + attribute(KotlinPlatformType.attribute, KotlinPlatformType.js) + } +} + +val compileTestDevelopmentExecutableKotlinJs = tasks.named("compileTestDevelopmentExecutableKotlinJs") + +val populateNodeModules = tasks.register("populateNodeModules") { + dependsOn("compileTestDevelopmentExecutableKotlinJs") + dependsOn(nodeModules) + from(compileTestDevelopmentExecutableKotlinJs.map { it.destinationDir }) + + from { + nodeModules.get().map { + // WORKAROUND: Some JS IR jars were absent and caused this task to fail. + // They don't contain .js thus we can skip them. + if (it.exists()) { + zipTree(it.absolutePath).matching { include("*.js") } + } else it + } + } + + into("${buildDir}/node_modules") +} + +fun createFrameworkTest(name: String): TaskProvider { + return tasks.register("test$name", NpmTask::class.java) { + dependsOn(compileTestDevelopmentExecutableKotlinJs, populateNodeModules, "npmInstall") + val lowerName = name.toLowerCase() + val tcOutput = "$buildDir/tc-${lowerName}.log" + val stdOutput = "$buildDir/test-${lowerName}.log" + val errOutput = "$buildDir/test-${lowerName}.err.log" +// inputs.files(sourceSets.test.output) + inputs.dir("${buildDir}/node_modules") + outputs.files(tcOutput, stdOutput, errOutput) + + setArgs(listOf("run", "test-$lowerName")) +// args("run") +// args("test-$lowerName") + group = "verification" + + setExecOverrides(closureOf { + isIgnoreExitValue = true + standardOutput = FileOutputStream(stdOutput) + errorOutput = FileOutputStream(errOutput) + }) + doLast { + println(file(tcOutput).readText()) + if (result.exitValue != 0/* && !rootProject.ignoreTestFailures*/) { + throw GradleException("$name integration test failed") + } + + } + } +} + +val frameworkTests = listOf( +// "Jest", + "Jasmine", + "Mocha", + "Qunit", +// "Tape" +).map { + createFrameworkTest(it) +} + +tasks.check { + frameworkTests.forEach { dependsOn(it) } +} + +dependencies { + api(project(":kotlin-test:kotlin-test-js-ir")) +} + +tasks.named("compileTestKotlinJs") { + dependsOn(jsMainSources) + dependsOn(jsSources) +} \ No newline at end of file diff --git a/libraries/kotlin.test/js-ir/it/package.json b/libraries/kotlin.test/js-ir/it/package.json new file mode 100644 index 00000000000..2d9ae0e7d88 --- /dev/null +++ b/libraries/kotlin.test/js-ir/it/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "test-jasmine": "jasmine build/jsSources/jasmine-reporter.js build/compileSync/test/testDevelopmentExecutable/kotlin/kotlin-kotlin-test-js-ir-it-test.js", + "test-jest": "jest", + "test-mocha": "mocha --reporter build/jsSources/mocha-reporter.js build/compileSync/test/testDevelopmentExecutable/kotlin/kotlin-kotlin-test-js-ir-it-test.js", + "test-qunit": "qunit -c build/jsSources/qunit-reporter.js -t build/compileSync/test/testDevelopmentExecutable/kotlin/kotlin-kotlin-test-js-ir-it-test.js", + "test-tape": "tape build/jsSources/tape-reporter.js build/jsSources/tape-plugin.js build/compileSync/test/testDevelopmentExecutable/kotlin/kotlin-kotlin-test-js-ir-it-test.js" + }, + "devDependencies": { + "jasmine": "^2.9.0", + "jest": "^20.0.4", + "mocha": "^3.4.2", + "qunit": "^1.0.0", + "tape": "~4.10.0" + }, + "jest": { + "testResultsProcessor": "/build/jsSources/jest-reporter.js", + "testRegex": "-test\\.js$" + } +} diff --git a/libraries/kotlin.test/js/it/src/test/kotlin/MainTest.kt b/libraries/kotlin.test/js/it/src/test/kotlin/MainTest.kt index b86c1a8a157..aced34a963e 100644 --- a/libraries/kotlin.test/js/it/src/test/kotlin/MainTest.kt +++ b/libraries/kotlin.test/js/it/src/test/kotlin/MainTest.kt @@ -44,14 +44,22 @@ class AsyncTest { var log = "" + var afterLog = "" + @BeforeTest fun before() { log = "" } + // Until bootstrap update + @AfterTest + fun after() { +// assertEquals(afterLog, "after") + } + fun promise(v: Int) = Promise { resolve, reject -> log += "a" - js("setTimeout")({ log += "c"; resolve(v) }, 100) + js("setTimeout")({ log += "c"; afterLog += "after"; resolve(v) }, 100) log += "b" } diff --git a/libraries/stdlib/api/js-v1/kotlin.js.kt b/libraries/stdlib/api/js-v1/kotlin.js.kt index 1879f48e564..1be8e0e27ae 100644 --- a/libraries/stdlib/api/js-v1/kotlin.js.kt +++ b/libraries/stdlib/api/js-v1/kotlin.js.kt @@ -283,6 +283,8 @@ public open external class Promise { public open fun catch(onRejected: (kotlin.Throwable) -> S): kotlin.js.Promise + public open fun finally(onFinally: () -> kotlin.Unit): kotlin.js.Promise + @kotlin.internal.LowPriorityInOverloadResolution public open fun then(onFulfilled: ((T) -> S)?): kotlin.js.Promise diff --git a/libraries/stdlib/api/js/kotlin.js.kt b/libraries/stdlib/api/js/kotlin.js.kt index 547b8eb87c7..c90a6fd6a84 100644 --- a/libraries/stdlib/api/js/kotlin.js.kt +++ b/libraries/stdlib/api/js/kotlin.js.kt @@ -282,6 +282,8 @@ public open external class Promise { public open fun catch(onRejected: (kotlin.Throwable) -> S): kotlin.js.Promise + public open fun finally(onFinally: () -> kotlin.Unit): kotlin.js.Promise + @kotlin.internal.LowPriorityInOverloadResolution public open fun then(onFulfilled: ((T) -> S)?): kotlin.js.Promise diff --git a/libraries/stdlib/js/src/kotlin/promise.kt b/libraries/stdlib/js/src/kotlin/promise.kt index 397eaadc180..e9d086364a5 100644 --- a/libraries/stdlib/js/src/kotlin/promise.kt +++ b/libraries/stdlib/js/src/kotlin/promise.kt @@ -20,6 +20,8 @@ public open external class Promise(executor: (resolve: (T) -> Unit, rejec public open fun catch(onRejected: (Throwable) -> S): Promise + public open fun finally(onFinally: () -> Unit): Promise + companion object { public fun all(promise: Array>): Promise> diff --git a/settings.gradle b/settings.gradle index 25b3636441e..77f96006c7f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -517,6 +517,7 @@ if (buildProperties.inJpsBuildIdeaSync) { ":kotlin-test:kotlin-test-js", ":kotlin-test:kotlin-test-js-ir", ":kotlin-test:kotlin-test-js:kotlin-test-js-it", + ":kotlin-test:kotlin-test-js-ir:kotlin-test-js-ir-it", ":kotlin-test:kotlin-test-wasm", ":native:native.tests" @@ -538,6 +539,7 @@ if (buildProperties.inJpsBuildIdeaSync) { project(':kotlin-test:kotlin-test-js').projectDir = "$rootDir/libraries/kotlin.test/js" as File project(':kotlin-test:kotlin-test-js-ir').projectDir = "$rootDir/libraries/kotlin.test/js-ir" as File project(':kotlin-test:kotlin-test-js:kotlin-test-js-it').projectDir = "$rootDir/libraries/kotlin.test/js/it" as File + project(':kotlin-test:kotlin-test-js-ir:kotlin-test-js-ir-it').projectDir = "$rootDir/libraries/kotlin.test/js-ir/it" as File project(':kotlin-test:kotlin-test-wasm').projectDir = "$rootDir/libraries/kotlin.test/wasm" as File project(':native:native.tests').projectDir = "$rootDir/native/native.tests" as File }