[Wasm] Initial K2 support in new test infrastructure (KT-57230)

- Implement FirWasmSessionFactory
- Use new compiler test infra for Wasm K1 and K2
- Delete old Wasm compiler test infra
This commit is contained in:
Svyatoslav Kuzmich
2023-06-12 11:23:31 +02:00
parent 3e564236f9
commit 9b3237fff9
40 changed files with 52444 additions and 4295 deletions
@@ -1,60 +1,29 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.generators.tests
import org.jetbrains.kotlin.generators.generateTestGroupSuiteWithJUnit5
import org.jetbrains.kotlin.generators.impl.generateTestGroupSuite
import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.wasm.test.*
import org.jetbrains.kotlin.wasm.test.diagnostics.AbstractDiagnosticsWasmTest
fun main(args: Array<String>) {
System.setProperty("java.awt.headless", "true")
// Common configuration shared between K1 and K2 tests:
val jvmOnlyBoxTests = listOf(
"compileKotlinAgainstKotlin",
)
generateTestGroupSuite(args) {
val jsTranslatorTestPattern = "^([^_](.+))\\.kt$"
testGroup("wasm/wasm.tests/tests-gen", "js/js.translator/testData", testRunnerMethodName = "runTest0") {
testClass<AbstractJsTranslatorWasmTest> {
model("box/main", pattern = jsTranslatorTestPattern, targetBackend = TargetBackend.WASM)
model("box/native/", pattern = jsTranslatorTestPattern, targetBackend = TargetBackend.WASM)
model("box/esModules/", pattern = jsTranslatorTestPattern, targetBackend = TargetBackend.WASM,
excludeDirs = listOf(
// JsExport is not supported for classes
"jsExport", "native", "export",
// Multimodal infra is not supported. Also, we don't use ES modules for cross-module refs in Wasm
"crossModuleRef", "crossModuleRefPerFile", "crossModuleRefPerModule"
)
)
model("box/jsQualifier/", pattern = jsTranslatorTestPattern, targetBackend = TargetBackend.WASM)
model("box/reflection/", pattern = "^(findAssociatedObject(InSeparatedFile)?)\\.kt$", targetBackend = TargetBackend.WASM)
}
testClass<AbstractJsTranslatorUnitWasmTest> {
model("box/kotlin.test/", pattern = jsTranslatorTestPattern, targetBackend = TargetBackend.WASM)
}
}
testGroup("wasm/wasm.tests/tests-gen", "compiler/testData", testRunnerMethodName = "runTest0") {
testClass<AbstractIrCodegenBoxWasmTest> {
model("codegen/box", targetBackend = TargetBackend.WASM, pattern = jsTranslatorTestPattern, excludeDirs = jvmOnlyBoxTests)
}
testClass<AbstractIrCodegenBoxInlineWasmTest> {
model("codegen/boxInline", targetBackend = TargetBackend.WASM)
}
testClass<AbstractIrCodegenWasmJsInteropWasmTest> {
model("codegen/boxWasmJsInterop", targetBackend = TargetBackend.WASM)
}
}
}
val jsTranslatorTestPattern = "^([^_](.+))\\.kt$"
val jsTranslatorReflectionPattern = "^(findAssociatedObject(InSeparatedFile)?)\\.kt$"
val jsTranslatorEsModulesExcludedDirs = listOf(
// JsExport is not supported for classes
"jsExport", "native", "export",
// Multimodal infra is not supported. Also, we don't use ES modules for cross-module refs in Wasm
"crossModuleRef", "crossModuleRefPerFile", "crossModuleRefPerModule"
)
generateTestGroupSuiteWithJUnit5(args) {
testGroup("wasm/wasm.tests/tests-gen", "compiler/testData") {
@@ -62,5 +31,55 @@ fun main(args: Array<String>) {
model("diagnostics/wasmTests")
}
}
testGroup("wasm/wasm.tests/tests-gen", "js/js.translator/testData", testRunnerMethodName = "runTest0") {
testClass<AbstractFirWasmJsTranslatorTest> {
model("box/main", pattern = jsTranslatorTestPattern)
model("box/native/", pattern = jsTranslatorTestPattern)
model("box/esModules/", pattern = jsTranslatorTestPattern, excludeDirs = jsTranslatorEsModulesExcludedDirs)
model("box/jsQualifier/", pattern = jsTranslatorTestPattern)
model("box/reflection/", pattern = jsTranslatorReflectionPattern)
model("box/kotlin.test/", pattern = jsTranslatorTestPattern)
}
}
testGroup("wasm/wasm.tests/tests-gen", "compiler/testData", testRunnerMethodName = "runTest0") {
testClass<AbstractFirWasmCodegenBoxTest> {
model("codegen/box", pattern = jsTranslatorTestPattern, excludeDirs = jvmOnlyBoxTests)
}
testClass<AbstractFirWasmCodegenBoxInlineTest> {
model("codegen/boxInline")
}
testClass<AbstractFirWasmCodegenWasmJsInteropTest> {
model("codegen/boxWasmJsInterop")
}
}
testGroup("wasm/wasm.tests/tests-gen", "js/js.translator/testData", testRunnerMethodName = "runTest0") {
testClass<AbstractK1WasmJsTranslatorTest> {
model("box/main", pattern = jsTranslatorTestPattern)
model("box/native/", pattern = jsTranslatorTestPattern)
model("box/esModules/", pattern = jsTranslatorTestPattern, excludeDirs = jsTranslatorEsModulesExcludedDirs)
model("box/jsQualifier/", pattern = jsTranslatorTestPattern)
model("box/reflection/", pattern = jsTranslatorReflectionPattern)
model("box/kotlin.test/", pattern = jsTranslatorTestPattern)
}
}
testGroup("wasm/wasm.tests/tests-gen", "compiler/testData", testRunnerMethodName = "runTest0") {
testClass<AbstractK1WasmCodegenBoxTest> {
model("codegen/box", pattern = jsTranslatorTestPattern, excludeDirs = jvmOnlyBoxTests)
}
testClass<AbstractK1WasmCodegenBoxInlineTest> {
model("codegen/boxInline")
}
testClass<AbstractK1WasmCodegenWasmJsInteropTest> {
model("codegen/boxWasmJsInterop")
}
}
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test
import org.jetbrains.kotlin.test.Constructor
import org.jetbrains.kotlin.test.FirParser
import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.test.backend.ir.IrBackendInput
import org.jetbrains.kotlin.test.builders.*
import org.jetbrains.kotlin.test.directives.DiagnosticsDirectives
import org.jetbrains.kotlin.test.directives.FirDiagnosticsDirectives
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives
import org.jetbrains.kotlin.test.frontend.fir.*
import org.jetbrains.kotlin.test.frontend.fir.handlers.*
import org.jetbrains.kotlin.test.model.*
import org.jetbrains.kotlin.test.runners.codegen.commonFirHandlersForCodegenTest
import org.jetbrains.kotlin.wasm.test.converters.FirWasmKlibBackendFacade
import org.jetbrains.kotlin.wasm.test.converters.WasmBackendFacade
open class AbstractFirWasmTest(
pathToTestDir: String,
testGroupOutputDirPrefix: String,
) : AbstractWasmBlackBoxCodegenTestBase<FirOutputArtifact, IrBackendInput, BinaryArtifacts.KLib>(
FrontendKinds.FIR, TargetBackend.WASM, pathToTestDir, testGroupOutputDirPrefix
) {
override val frontendFacade: Constructor<FrontendFacade<FirOutputArtifact>>
get() = ::FirFrontendFacade
override val frontendToBackendConverter: Constructor<Frontend2BackendConverter<FirOutputArtifact, IrBackendInput>>
get() = ::Fir2IrWasmResultsConverter
override val backendFacade: Constructor<BackendFacade<IrBackendInput, BinaryArtifacts.KLib>>
get() = ::FirWasmKlibBackendFacade
override val afterBackendFacade: Constructor<AbstractTestFacade<BinaryArtifacts.KLib, BinaryArtifacts.Wasm>>
get() = ::WasmBackendFacade
override fun configure(builder: TestConfigurationBuilder) {
super.configure(builder)
with(builder) {
defaultDirectives {
+LanguageSettingsDirectives.ALLOW_KOTLIN_PACKAGE
DiagnosticsDirectives.DIAGNOSTICS with listOf("-infos")
FirDiagnosticsDirectives.FIR_PARSER with FirParser.Psi
}
firHandlersStep {
useHandlers(
::FirDumpHandler,
::FirCfgDumpHandler,
::FirCfgConsistencyHandler,
::FirResolvedTypesVerifier,
)
}
}
}
}
open class AbstractFirWasmCodegenBoxTest : AbstractFirWasmTest(
pathToTestDir = "compiler/testData/codegen/box/",
testGroupOutputDirPrefix = "codegen/firBox/"
) {
override fun configure(builder: TestConfigurationBuilder) {
super.configure(builder)
builder.configureFirHandlersStep {
commonFirHandlersForCodegenTest()
}
builder.useAfterAnalysisCheckers(
::FirMetaInfoDiffSuppressor
)
}
}
open class AbstractFirWasmCodegenBoxInlineTest : AbstractFirWasmTest(
"compiler/testData/codegen/boxInline/",
"codegen/firBoxInline/"
)
open class AbstractFirWasmCodegenWasmJsInteropTest : AbstractFirWasmTest(
"compiler/testData/codegen/wasmJsInterop",
"codegen/firWasmJsInterop"
)
open class AbstractFirWasmJsTranslatorTest : AbstractFirWasmTest(
"js/js.translator/testData/box/",
"js.translator/firBox"
)
@@ -0,0 +1,55 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test
import org.jetbrains.kotlin.test.Constructor
import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.test.backend.ir.IrBackendInput
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontend2IrConverter
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontendFacade
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontendOutputArtifact
import org.jetbrains.kotlin.test.model.*
import org.jetbrains.kotlin.wasm.test.converters.FirWasmKlibBackendFacade
import org.jetbrains.kotlin.wasm.test.converters.WasmBackendFacade
abstract class AbstractK1WasmTest(
pathToTestDir: String,
testGroupOutputDirPrefix: String,
) : AbstractWasmBlackBoxCodegenTestBase<ClassicFrontendOutputArtifact, IrBackendInput, BinaryArtifacts.KLib>(
FrontendKinds.ClassicFrontend, TargetBackend.WASM, pathToTestDir, testGroupOutputDirPrefix
) {
override val frontendFacade: Constructor<FrontendFacade<ClassicFrontendOutputArtifact>>
get() = ::ClassicFrontendFacade
override val frontendToBackendConverter: Constructor<Frontend2BackendConverter<ClassicFrontendOutputArtifact, IrBackendInput>>
get() = ::ClassicFrontend2IrConverter
override val backendFacade: Constructor<BackendFacade<IrBackendInput, BinaryArtifacts.KLib>>
get() = ::FirWasmKlibBackendFacade
override val afterBackendFacade: Constructor<AbstractTestFacade<BinaryArtifacts.KLib, BinaryArtifacts.Wasm>>
get() = ::WasmBackendFacade
}
open class AbstractK1WasmCodegenBoxTest : AbstractK1WasmTest(
"compiler/testData/codegen/box/",
"codegen/k1Box/"
)
open class AbstractK1WasmCodegenBoxInlineTest : AbstractK1WasmTest(
"compiler/testData/codegen/boxInline/",
"codegen/k1BoxInline/"
)
open class AbstractK1WasmCodegenWasmJsInteropTest : AbstractK1WasmTest(
"compiler/testData/codegen/wasmJsInterop",
"codegen/k1WasmJsInteropBox"
)
open class AbstractK1WasmJsTranslatorTest : AbstractK1WasmTest(
"js/js.translator/testData/box/",
"js.translator/k1Box"
)
@@ -0,0 +1,112 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test
import org.jetbrains.kotlin.platform.wasm.WasmPlatforms
import org.jetbrains.kotlin.test.Constructor
import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.test.backend.BlackBoxCodegenSuppressor
import org.jetbrains.kotlin.test.backend.handlers.KlibInterpreterDumpHandler
import org.jetbrains.kotlin.test.backend.handlers.WasmIrInterpreterDumpHandler
import org.jetbrains.kotlin.test.builders.*
import org.jetbrains.kotlin.test.directives.DiagnosticsDirectives
import org.jetbrains.kotlin.test.directives.DiagnosticsDirectives.DIAGNOSTICS
import org.jetbrains.kotlin.test.directives.WasmEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.frontend.classic.handlers.ClassicDiagnosticsHandler
import org.jetbrains.kotlin.test.frontend.fir.handlers.FirDiagnosticsHandler
import org.jetbrains.kotlin.test.model.*
import org.jetbrains.kotlin.test.runners.AbstractKotlinCompilerWithTargetBackendTest
import org.jetbrains.kotlin.test.runners.codegen.commonClassicFrontendHandlersForCodegenTest
import org.jetbrains.kotlin.test.services.LibraryProvider
import org.jetbrains.kotlin.test.services.configuration.CommonEnvironmentConfigurator
import org.jetbrains.kotlin.test.services.configuration.WasmEnvironmentConfigurator
import org.jetbrains.kotlin.test.services.sourceProviders.CoroutineHelpersSourceFilesProvider
import org.jetbrains.kotlin.wasm.test.handlers.WasmBoxRunner
abstract class AbstractWasmBlackBoxCodegenTestBase<R : ResultingArtifact.FrontendOutput<R>, I : ResultingArtifact.BackendInput<I>, A : ResultingArtifact.Binary<A>>(
private val targetFrontend: FrontendKind<R>,
targetBackend: TargetBackend,
private val pathToTestDir: String,
private val testGroupOutputDirPrefix: String,
) : AbstractKotlinCompilerWithTargetBackendTest(targetBackend) {
abstract val frontendFacade: Constructor<FrontendFacade<R>>
abstract val frontendToBackendConverter: Constructor<Frontend2BackendConverter<R, I>>
abstract val backendFacade: Constructor<BackendFacade<I, A>>
abstract val afterBackendFacade: Constructor<AbstractTestFacade<A, BinaryArtifacts.Wasm>>
override fun TestConfigurationBuilder.configuration() {
globalDefaults {
frontend = targetFrontend
targetPlatform = WasmPlatforms.Default
dependencyKind = DependencyKind.Binary
}
val pathToRootOutputDir = System.getProperty("kotlin.wasm.test.root.out.dir") ?: error("'kotlin.wasm.test.root.out.dir' is not set")
defaultDirectives {
+DiagnosticsDirectives.REPORT_ONLY_EXPLICITLY_DEFINED_DEBUG_INFO
WasmEnvironmentConfigurationDirectives.PATH_TO_ROOT_OUTPUT_DIR with pathToRootOutputDir
WasmEnvironmentConfigurationDirectives.PATH_TO_TEST_DIR with pathToTestDir
WasmEnvironmentConfigurationDirectives.TEST_GROUP_OUTPUT_DIR_PREFIX with testGroupOutputDirPrefix
}
forTestsNotMatching("compiler/testData/codegen/box/diagnostics/functions/tailRecursion/*") {
defaultDirectives {
DIAGNOSTICS with "-warnings"
}
}
forTestsNotMatching("compiler/testData/codegen/boxError/*") {
enableMetaInfoHandler()
}
useConfigurators(
::WasmEnvironmentConfigurator,
)
useAdditionalSourceProviders(
::WasmAdditionalSourceProvider,
::CoroutineHelpersSourceFilesProvider,
)
useAdditionalService(::LibraryProvider)
useAfterAnalysisCheckers(
::WasmFailingTestSuppressor,
::BlackBoxCodegenSuppressor,
)
facadeStep(frontendFacade)
classicFrontendHandlersStep {
commonClassicFrontendHandlersForCodegenTest()
useHandlers(::ClassicDiagnosticsHandler)
}
firHandlersStep {
useHandlers(::FirDiagnosticsHandler)
}
facadeStep(frontendToBackendConverter)
irHandlersStep()
facadeStep(backendFacade)
klibArtifactsHandlersStep()
facadeStep(afterBackendFacade)
forTestsMatching("compiler/testData/codegen/box/involvesIrInterpreter/*") {
enableMetaInfoHandler()
configureKlibArtifactsHandlersStep {
useHandlers(::KlibInterpreterDumpHandler)
}
configureWasmArtifactsHandlersStep {
useHandlers(::WasmIrInterpreterDumpHandler)
}
}
wasmArtifactsHandlersStep {
useHandlers(::WasmBoxRunner)
}
}
}
@@ -1,416 +0,0 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.ObsoleteTestInfrastructure
import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
import org.jetbrains.kotlin.backend.common.phaser.toPhaseMap
import org.jetbrains.kotlin.backend.wasm.*
import org.jetbrains.kotlin.backend.wasm.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.checkers.parseLanguageVersionSettings
import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport
import org.jetbrains.kotlin.cli.js.klib.TopDownAnalyzerFacadeForWasm
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.*
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.ir.backend.js.dce.dumpDeclarationIrSizesIfNeed
import org.jetbrains.kotlin.ir.backend.js.prepareAnalyzedSourceModule
import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImpl
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.config.JsConfig
import org.jetbrains.kotlin.js.facade.TranslationUnit
import org.jetbrains.kotlin.wasm.test.tools.WasmVM
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.resolve.CompilerEnvironment
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.test.*
import org.jetbrains.kotlin.test.util.KtTestUtil
import java.io.Closeable
import java.io.File
abstract class BasicWasmBoxTest(
private val pathToTestDir: String,
testGroupOutputDirPrefix: String,
private val startUnitTests: Boolean = false
) : KotlinTestWithEnvironment() {
private val pathToRootOutputDir: String = System.getProperty("kotlin.wasm.test.root.out.dir") ?: error("'kotlin.wasm.test.root.out.dir' is not set")
private val testGroupOutputDirForCompilation = File(pathToRootOutputDir + "out/" + testGroupOutputDirPrefix)
private val COMMON_FILES_NAME = "_common"
private val COMMON_FILES_DIR = "_commonFiles"
private val extraLanguageFeatures = mapOf(
LanguageFeature.JsAllowImplementingFunctionInterface to LanguageFeature.State.ENABLED,
)
fun doTest(filePath: String) = doTestWithTransformer(filePath) { it }
@OptIn(ObsoleteTestInfrastructure::class)
fun doTestWithTransformer(filePath: String, transformer: java.util.function.Function<String, String>) {
val file = File(filePath)
val outputDirBase = File(getOutputDir(file), getTestName(true))
val fileContent = transformer.apply(KtTestUtil.doLoadFile(file))
TestFileFactoryImpl().use { testFactory ->
val inputFiles: MutableList<TestFile> = TestFiles.createTestFiles(file.name, fileContent, testFactory, true)
val testPackage = testFactory.testPackage
val languageVersionSettings = inputFiles.firstNotNullOfOrNull { it.languageVersionSettings }
val kotlinFiles = mutableListOf<String>()
val jsFiles = mutableListOf<String>()
val mjsFiles = mutableListOf<String>()
var entryMjs: String? = "test.mjs"
inputFiles.forEach {
val name = it.fileName
when {
name.endsWith(".kt") ->
kotlinFiles += name
name.endsWith(".js") ->
jsFiles += name
name.endsWith(".mjs") -> {
mjsFiles += name
val fileName = File(name).name
if (fileName == "entry.mjs") {
entryMjs = fileName
}
}
}
}
val additionalJsFile = filePath.removeSuffix(".kt") + ".js"
if (File(additionalJsFile).exists()) {
jsFiles += additionalJsFile
}
val additionalMjsFile = filePath.removeSuffix(".kt") + ".mjs"
if (File(additionalMjsFile).exists()) {
mjsFiles += additionalMjsFile
}
val localCommonFile = file.parent + "/" + COMMON_FILES_NAME + "." + KotlinFileType.EXTENSION
val localCommonFiles = if (File(localCommonFile).exists()) listOf(localCommonFile) else emptyList()
val globalCommonFilesDir = File(File(pathToTestDir).parent, COMMON_FILES_DIR)
val globalCommonFiles = globalCommonFilesDir.listFiles().orEmpty().map { it.absolutePath }
val allSourceFiles = kotlinFiles + localCommonFiles + globalCommonFiles
val psiFiles = createPsiFiles(allSourceFiles.map { File(it).canonicalPath }.sorted())
val config = createConfig(languageVersionSettings)
val filesToCompile = psiFiles.map { TranslationUnit.SourceFile(it).file }
val debugMode = DebugMode.fromSystemProperty("kotlin.wasm.debugMode")
val phaseConfig = if (debugMode >= DebugMode.SUPER_DEBUG) {
val dumpOutputDir = File(outputDirBase, "irdump")
println("\n ------ Dumping phases to file://${dumpOutputDir.absolutePath}")
PhaseConfig(
wasmPhases,
dumpToDirectory = dumpOutputDir.path,
toDumpStateAfter = wasmPhases.toPhaseMap().values.toSet(),
)
} else {
PhaseConfig(wasmPhases)
}
if (debugMode >= DebugMode.DEBUG) {
println(" ------ KT file://${file.absolutePath}")
}
val sourceModule = prepareAnalyzedSourceModule(
config.project,
filesToCompile,
config.configuration,
// TODO: Bypass the resolver fow wasm.
listOf(System.getProperty("kotlin.wasm.stdlib.path")!!, System.getProperty("kotlin.wasm.kotlin.test.path")!!),
emptyList(),
AnalyzerWithCompilerReport(config.configuration),
analyzerFacade = TopDownAnalyzerFacadeForWasm
)
val (allModules, backendContext) = compileToLoweredIr(
depsDescriptors = sourceModule,
phaseConfig = phaseConfig,
irFactory = IrFactoryImpl,
exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, TEST_FUNCTION))),
propertyLazyInitialization = true,
)
val generateWat = debugMode >= DebugMode.DEBUG
val baseFileName = "index"
val compilerResult = compileWasm(
allModules = allModules,
backendContext = backendContext,
baseFileName = baseFileName,
emitNameSection = true,
allowIncompleteImplementations = false,
generateWat = generateWat,
)
eliminateDeadDeclarations(allModules, backendContext)
dumpDeclarationIrSizesIfNeed(System.getProperty("kotlin.wasm.dump.declaration.ir.size.to.file"), allModules)
val compilerResultWithDCE = compileWasm(
allModules = allModules,
backendContext = backendContext,
baseFileName = baseFileName,
emitNameSection = true,
allowIncompleteImplementations = true,
generateWat = generateWat,
)
val testJsQuiet = """
let actualResult;
try {
// Use "dynamic import" to catch exception happened during JS & Wasm modules initialization
let jsModule = await import('./index.mjs');
let wasmExports = jsModule.default;
${if (startUnitTests) "wasmExports.startUnitTests();" else ""}
actualResult = wasmExports.box();
} catch(e) {
console.log('Failed with exception!')
console.log('Message: ' + e.message)
console.log('Name: ' + e.name)
console.log('Stack:')
console.log(e.stack)
}
if (actualResult !== "OK")
throw `Wrong box result '${'$'}{actualResult}'; Expected "OK"`;
""".trimIndent()
val testJsVerbose = testJsQuiet + """
console.log('test passed');
""".trimIndent()
val testJs = if (debugMode >= DebugMode.DEBUG) testJsVerbose else testJsQuiet
fun printPathAndSize(mode: String, fileKind: String, path: String, name: String) {
val size = File("$path/$name").length()
println(" ------ $mode $fileKind file://$path/$name $size B")
}
fun checkExpectedOutputSize(testFileContent: String, testDir: File) {
val expectedSizes =
InTextDirectivesUtils.findListWithPrefixes(testFileContent, "// WASM_DCE_EXPECTED_OUTPUT_SIZE: ")
.map {
val i = it.indexOf(' ')
val extension = it.substring(0, i)
val size = it.substring(i + 1)
extension.trim().lowercase() to size.filter(Char::isDigit).toInt()
}
val filesByExtension = testDir.listFiles()?.groupBy { it.extension }.orEmpty()
val errors = expectedSizes.mapNotNull { (extension, expectedSize) ->
val totalSize = filesByExtension[extension].orEmpty().sumOf { it.length() }
val thresholdPercent = 1
val thresholdInBytes = expectedSize * thresholdPercent / 100
val expectedMinSize = expectedSize - thresholdInBytes
val expectedMaxSize = expectedSize + thresholdInBytes
val diff = totalSize - expectedSize
val message = "Total size of $extension files is $totalSize," +
" but expected $expectedSize$thresholdInBytes [$expectedMinSize .. $expectedMaxSize]." +
" Diff: $diff (${diff * 100 / expectedSize}%)"
if (debugMode >= DebugMode.DEBUG) {
println(" ------ $message")
}
if (totalSize !in expectedMinSize..expectedMaxSize) message else null
}
if (errors.isNotEmpty()) throw AssertionError(errors.joinToString("\n"))
}
fun writeToFilesAndRunTest(mode: String, res: WasmCompilerResult) {
val dir = File(outputDirBase, mode)
dir.mkdirs()
writeCompilationResult(res, dir, baseFileName)
File(dir, "test.mjs").writeText(testJs)
for (mjsPath: String in mjsFiles) {
val mjsFile = File(mjsPath)
File(dir, mjsFile.name).writeText(mjsFile.readText())
}
if (debugMode >= DebugMode.DEBUG) {
File(dir, "index.html").writeText(
"""
<!DOCTYPE html>
<html lang="en">
<body>
<span id="test">UNKNOWN</span>
<script type="module">
let test = document.getElementById("test")
try {
await import("./test.mjs");
test.style.backgroundColor = "#0f0";
test.textContent = "OK"
} catch(e) {
test.style.backgroundColor = "#f00";
test.textContent = "NOT OK"
throw e;
}
</script>
</body>
</html>
""".trimIndent()
)
val path = dir.absolutePath
println(" ------ $mode Wat file://$path/index.wat")
println(" ------ $mode Wasm file://$path/index.wasm")
println(" ------ $mode JS file://$path/index.uninstantiated.mjs")
println(" ------ $mode JS file://$path/index.mjs")
println(" ------ $mode Test file://$path/test.mjs")
val projectName = "kotlin"
println(" ------ $mode HTML http://0.0.0.0:63342/$projectName/${dir.path}/index.html")
for (mjsPath: String in mjsFiles) {
println(" ------ $mode External ESM file://$path/${File(mjsPath).name}")
}
}
val testFile = file.readText()
val failsIn = InTextDirectivesUtils.findListWithPrefixes(testFile, "// WASM_FAILS_IN: ")
val exceptions = listOf(WasmVM.V8, WasmVM.SpiderMonkey).mapNotNull map@{ vm ->
try {
if (debugMode >= DebugMode.DEBUG) {
println(" ------ Run in ${vm.name}" + if (vm.shortName in failsIn) " (expected to fail)" else "")
}
vm.run(
"./${entryMjs}",
jsFiles.map { File(it).absolutePath },
workingDirectory = dir
)
if (vm.shortName in failsIn) {
return@map AssertionError("The test expected to fail in ${vm.name}. Please update the testdata.")
}
} catch (e: Throwable) {
if (vm.shortName !in failsIn) {
return@map e
}
}
null
}
when (exceptions.size) {
0 -> {} // Everything OK
1 -> {
throw exceptions.single()
}
else -> {
throw AssertionError("Failed with several exceptions. Look at suppressed exceptions below.").apply {
exceptions.forEach { addSuppressed(it) }
}
}
}
if (mode == "dce") {
checkExpectedOutputSize(testFile, dir)
}
}
writeToFilesAndRunTest("dev", compilerResult)
writeToFilesAndRunTest("dce", compilerResultWithDCE)
}
}
private fun getOutputDir(file: File, testGroupOutputDir: File = testGroupOutputDirForCompilation): File {
val stopFile = File(pathToTestDir)
return generateSequence(file.parentFile) { it.parentFile }
.takeWhile { it != stopFile }
.map { it.name }
.toList().asReversed()
.fold(testGroupOutputDir, ::File)
}
private fun createConfig(languageVersionSettings: LanguageVersionSettings?): JsConfig {
val configuration = environment.configuration.copy()
configuration.put(CommonConfigurationKeys.MODULE_NAME, TEST_MODULE)
configuration.put(JSConfigurationKeys.WASM_ENABLE_ARRAY_RANGE_CHECKS, true)
configuration.put(JSConfigurationKeys.WASM_ENABLE_ASSERTS, true)
configuration.put(JSConfigurationKeys.MODULE_KIND, ModuleKind.ES)
configuration.languageVersionSettings = languageVersionSettings
?: LanguageVersionSettingsImpl(LanguageVersion.LATEST_STABLE, ApiVersion.LATEST_STABLE, specificFeatures = extraLanguageFeatures)
return JsConfig(project, configuration, CompilerEnvironment, null, null)
}
@OptIn(ObsoleteTestInfrastructure::class)
private inner class TestFileFactoryImpl : TestFiles.TestFileFactoryNoModules<TestFile>(), Closeable {
override fun create(fileName: String, text: String, directives: Directives): TestFile {
val ktFile = KtPsiFactory(project).createFile(text)
val boxFunction = ktFile.declarations.find { it is KtNamedFunction && it.name == TEST_FUNCTION }
if (boxFunction != null) {
testPackage = ktFile.packageFqName.asString()
if (testPackage?.isEmpty() == true) {
testPackage = null
}
}
val languageVersionSettings = parseLanguageVersionSettings(directives, extraLanguageFeatures)
val temporaryFile = File(tmpDir, "WASM_TEST/$fileName")
KtTestUtil.mkdirs(temporaryFile.parentFile)
temporaryFile.writeText(text, Charsets.UTF_8)
return TestFile(temporaryFile.absolutePath, languageVersionSettings)
}
var testPackage: String? = null
val tmpDir = KtTestUtil.tmpDir("wasm-tests")
override fun close() {
FileUtil.delete(tmpDir)
}
}
private class TestFile(val fileName: String, val languageVersionSettings: LanguageVersionSettings?)
override fun createEnvironment() =
KotlinCoreEnvironment.createForTests(testRootDisposable, CompilerConfiguration(), EnvironmentConfigFiles.JS_CONFIG_FILES)
private fun KotlinTestWithEnvironment.createPsiFile(fileName: String): KtFile {
val psiManager = PsiManager.getInstance(project)
val fileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
val file = fileSystem.findFileByPath(fileName) ?: error("File not found: $fileName")
return psiManager.findFile(file) as KtFile
}
private fun KotlinTestWithEnvironment.createPsiFiles(fileNames: List<String>): List<KtFile> {
return fileNames.map { this@createPsiFiles.createPsiFile(it) }
}
companion object {
const val TEST_MODULE = "main"
private const val TEST_FUNCTION = "box"
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.test.directives.JsEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.AdditionalSourceProvider
import org.jetbrains.kotlin.test.services.TestServices
import java.io.File
import java.io.FileFilter
class WasmAdditionalSourceProvider(testServices: TestServices) : AdditionalSourceProvider(testServices) {
override fun produceAdditionalFiles(globalDirectives: RegisteredDirectives, module: TestModule): List<TestFile> {
if (JsEnvironmentConfigurationDirectives.NO_COMMON_FILES in module.directives) return emptyList()
return getAdditionalKotlinFiles(module.files.first().originalFile.parent).map { it.toTestFile() }
}
companion object {
private const val COMMON_FILES_NAME = "_common"
private const val COMMON_FILES_DIR = "_commonFiles/"
private const val COMMON_FILES_DIR_PATH = "js/js.translator/testData/$COMMON_FILES_DIR"
private fun getFilesInDirectoryByExtension(directory: String, extension: String): List<String> {
val dir = File(directory)
if (!dir.isDirectory) return emptyList()
return dir.listFiles(FileFilter { it.extension == extension })?.map { it.absolutePath } ?: emptyList()
}
private fun getAdditionalFiles(directory: String, extension: String): List<File> {
val globalCommonFiles = getFilesInDirectoryByExtension(COMMON_FILES_DIR_PATH, extension).map { File(it) }
val localCommonFilePath = "$directory/$COMMON_FILES_NAME.$extension"
val localCommonFile = File(localCommonFilePath).takeIf { it.exists() } ?: return globalCommonFiles
return globalCommonFiles + localCommonFile
}
fun getAdditionalKotlinFiles(directory: String): List<File> {
return getAdditionalFiles(directory, KotlinFileType.EXTENSION)
}
}
}
@@ -0,0 +1,21 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test
import org.jetbrains.kotlin.test.WrappedException
import org.jetbrains.kotlin.test.model.AfterAnalysisChecker
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.moduleStructure
class WasmFailingTestSuppressor(testServices: TestServices) : AfterAnalysisChecker(testServices) {
override fun suppressIfNeeded(failedAssertions: List<WrappedException>): List<WrappedException> {
val testFile = testServices.moduleStructure.originalTestDataFiles.first()
val failFile = testFile.parentFile.resolve("${testFile.name}.fail").takeIf { it.exists() }
?: return failedAssertions
if (failedAssertions.any { it is WrappedException.FromFacade }) return emptyList()
return failedAssertions + AssertionError("Fail file exists but no exception was thrown. Please remove ${failFile.name}").wrap()
}
}
@@ -1,34 +0,0 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test
import org.jetbrains.kotlin.wasm.test.BasicWasmBoxTest
abstract class AbstractIrCodegenBoxWasmTest : BasicWasmBoxTest(
"compiler/testData/codegen/box/",
"codegen/wasmBox/"
)
abstract class AbstractIrCodegenBoxInlineWasmTest : BasicWasmBoxTest(
"compiler/testData/codegen/boxInline/",
"codegen/wasmBoxInline/"
)
abstract class AbstractIrCodegenWasmJsInteropWasmTest : BasicWasmBoxTest(
"compiler/testData/codegen/wasmJsInterop",
"codegen/wasmJsInteropJs"
)
abstract class AbstractJsTranslatorWasmTest : BasicWasmBoxTest(
"js/js.translator/testData/box/",
"js.translator/wasmBox"
)
abstract class AbstractJsTranslatorUnitWasmTest : BasicWasmBoxTest(
"js/js.translator/testData/box/",
"js.translator/wasmBox",
startUnitTests = true
)
@@ -0,0 +1,123 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test.converters
import org.jetbrains.kotlin.backend.common.CommonKLibResolver
import org.jetbrains.kotlin.backend.common.actualizer.IrActualizer
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl
import org.jetbrains.kotlin.diagnostics.DiagnosticReporterFactory
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.ir.backend.js.JsFactories
import org.jetbrains.kotlin.ir.backend.js.resolverLogger
import org.jetbrains.kotlin.ir.backend.js.serializeModuleIntoKlib
import org.jetbrains.kotlin.ir.types.IrTypeSystemContextImpl
import org.jetbrains.kotlin.ir.util.IrMessageLogger
import org.jetbrains.kotlin.library.KotlinAbiVersion
import org.jetbrains.kotlin.storage.LockBasedStorageManager
import org.jetbrains.kotlin.test.backend.ir.IrBackendFacade
import org.jetbrains.kotlin.test.backend.ir.IrBackendInput
import org.jetbrains.kotlin.test.frontend.classic.ModuleDescriptorProvider
import org.jetbrains.kotlin.test.frontend.classic.moduleDescriptorProvider
import org.jetbrains.kotlin.test.frontend.fir.getAllWasmDependenciesPaths
import org.jetbrains.kotlin.test.frontend.fir.resolveLibraries
import org.jetbrains.kotlin.test.model.ArtifactKinds
import org.jetbrains.kotlin.test.model.BinaryArtifacts
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.*
import org.jetbrains.kotlin.test.services.configuration.WasmEnvironmentConfigurator
import java.io.File
class FirWasmKlibBackendFacade(
testServices: TestServices,
private val firstTimeCompilation: Boolean
) : IrBackendFacade<BinaryArtifacts.KLib>(testServices, ArtifactKinds.KLib) {
override val additionalServices: List<ServiceRegistrationData>
get() = listOf(service(::ModuleDescriptorProvider))
constructor(testServices: TestServices) : this(testServices, firstTimeCompilation = true)
override fun shouldRunAnalysis(module: TestModule): Boolean {
return module.backendKind == inputKind
}
override fun transform(module: TestModule, inputArtifact: IrBackendInput): BinaryArtifacts.KLib {
require(inputArtifact is IrBackendInput.WasmBackendInput) {
"FirWasmKlibBackendFacade expects IrBackendInput.WasmBackendInput as input"
}
val configuration = testServices.compilerConfigurationProvider.getCompilerConfiguration(module)
val outputFile = WasmEnvironmentConfigurator.getWasmKlibArtifactPath(testServices, module.name)
// TODO: consider avoiding repeated libraries resolution
val libraries = resolveLibraries(configuration, getAllWasmDependenciesPaths(module, testServices))
// TODO: find out how to pass diagnostics to the test infra in this case
val diagnosticReporter = DiagnosticReporterFactory.createReporter()
if (firstTimeCompilation) {
val irActualizedResult =
if (module.frontendKind == FrontendKinds.FIR && module.languageVersionSettings.supportsFeature(LanguageFeature.MultiPlatformProjects)) {
IrActualizer.actualize(
inputArtifact.irModuleFragment,
inputArtifact.dependentIrModuleFragments,
diagnosticReporter,
IrTypeSystemContextImpl(inputArtifact.irModuleFragment.irBuiltins),
configuration.languageVersionSettings
)
} else {
null
}
serializeModuleIntoKlib(
configuration[CommonConfigurationKeys.MODULE_NAME]!!,
configuration,
configuration.get(IrMessageLogger.IR_MESSAGE_LOGGER) ?: IrMessageLogger.None,
inputArtifact.sourceFiles,
klibPath = outputFile,
libraries.map { it.library },
inputArtifact.irModuleFragment,
inputArtifact.expectDescriptorToSymbol,
cleanFiles = inputArtifact.icData,
nopack = true,
perFile = false,
containsErrorCode = inputArtifact.hasErrors,
abiVersion = KotlinAbiVersion.CURRENT, // TODO get from test file data
jsOutputName = null
) {
inputArtifact.serializeSingleFile(it, irActualizedResult)
}
}
// TODO: consider avoiding repeated libraries resolution
val lib = CommonKLibResolver.resolve(
getAllWasmDependenciesPaths(module, testServices) + listOf(outputFile),
configuration.resolverLogger
).getFullResolvedList().last().library
val moduleDescriptor = JsFactories.DefaultDeserializedDescriptorFactory.createDescriptorOptionalBuiltIns(
lib,
configuration.languageVersionSettings,
LockBasedStorageManager("ModulesStructure"),
inputArtifact.irModuleFragment.descriptor.builtIns,
packageAccessHandler = null,
lookupTracker = LookupTracker.DO_NOTHING
)
moduleDescriptor.setDependencies(
inputArtifact.irModuleFragment.descriptor.allDependencyModules.filterIsInstance<ModuleDescriptorImpl>() + moduleDescriptor
)
testServices.moduleDescriptorProvider.replaceModuleDescriptorForModule(module, moduleDescriptor)
testServices.libraryProvider.setDescriptorAndLibraryByName(outputFile, moduleDescriptor, lib)
return BinaryArtifacts.KLib(File(outputFile))
}
}
@@ -0,0 +1,139 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test.converters
import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
import org.jetbrains.kotlin.backend.common.phaser.toPhaseMap
import org.jetbrains.kotlin.backend.wasm.compileToLoweredIr
import org.jetbrains.kotlin.backend.wasm.compileWasm
import org.jetbrains.kotlin.backend.wasm.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.backend.wasm.wasmPhases
import org.jetbrains.kotlin.ir.backend.js.MainModule
import org.jetbrains.kotlin.ir.backend.js.ModulesStructure
import org.jetbrains.kotlin.ir.backend.js.dce.dumpDeclarationIrSizesIfNeed
import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImpl
import org.jetbrains.kotlin.ir.linkage.partial.PartialLinkageConfig
import org.jetbrains.kotlin.ir.linkage.partial.PartialLinkageLogLevel
import org.jetbrains.kotlin.ir.linkage.partial.PartialLinkageMode
import org.jetbrains.kotlin.ir.linkage.partial.setupPartialLinkageConfig
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.test.DebugMode
import org.jetbrains.kotlin.test.model.AbstractTestFacade
import org.jetbrains.kotlin.test.model.ArtifactKinds
import org.jetbrains.kotlin.test.model.BinaryArtifacts
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.*
import org.jetbrains.kotlin.test.services.configuration.WasmEnvironmentConfigurator
import org.jetbrains.kotlin.wasm.test.handlers.getWasmTestOutputDirectory
import java.io.File
class WasmBackendFacade(
private val testServices: TestServices
) : AbstractTestFacade<BinaryArtifacts.KLib, BinaryArtifacts.Wasm>() {
override val inputKind = ArtifactKinds.KLib
override val outputKind = ArtifactKinds.Wasm
override fun transform(module: TestModule, inputArtifact: BinaryArtifacts.KLib): BinaryArtifacts.Wasm? {
val configuration = testServices.compilerConfigurationProvider.getCompilerConfiguration(module)
// Enforce PL with the ERROR log level to fail any tests where PL detected any incompatibilities.
configuration.setupPartialLinkageConfig(PartialLinkageConfig(PartialLinkageMode.ENABLE, PartialLinkageLogLevel.ERROR))
val isMainModule = WasmEnvironmentConfigurator.isMainModule(module, testServices)
if (!isMainModule) return null
val debugMode = DebugMode.fromSystemProperty("kotlin.wasm.debugMode")
val phaseConfig = if (debugMode >= DebugMode.SUPER_DEBUG) {
val outputDirBase = testServices.getWasmTestOutputDirectory()
val dumpOutputDir = File(outputDirBase, "irdump")
println("\n ------ Dumping phases to file://${dumpOutputDir.absolutePath}")
PhaseConfig(
wasmPhases,
dumpToDirectory = dumpOutputDir.path,
toDumpStateAfter = wasmPhases.toPhaseMap().values.toSet(),
)
} else {
PhaseConfig(wasmPhases)
}
val libraries = listOf(
System.getProperty("kotlin.wasm.stdlib.path")!!,
System.getProperty("kotlin.wasm.kotlin.test.path")!!
) + WasmEnvironmentConfigurator.getAllRecursiveLibrariesFor(module, testServices).map { it.key.libraryFile.canonicalPath }
val friendLibraries = emptyList<String>()
val mainModule = MainModule.Klib(inputArtifact.outputFile.absolutePath)
val project = testServices.compilerConfigurationProvider.getProject(module)
val moduleStructure = ModulesStructure(
project,
mainModule,
configuration,
libraries + mainModule.libPath,
friendLibraries
)
val testPackage = extractTestPackage(testServices)
val (allModules, backendContext) = compileToLoweredIr(
depsDescriptors = moduleStructure,
phaseConfig = phaseConfig,
irFactory = IrFactoryImpl,
exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, "box"))),
propertyLazyInitialization = true,
)
val generateWat = debugMode >= DebugMode.DEBUG
val baseFileName = "index"
val compilerResult = compileWasm(
allModules = allModules,
backendContext = backendContext,
baseFileName = baseFileName,
emitNameSection = true,
allowIncompleteImplementations = false,
generateWat = generateWat,
)
eliminateDeadDeclarations(allModules, backendContext)
dumpDeclarationIrSizesIfNeed(System.getProperty("kotlin.wasm.dump.declaration.ir.size.to.file"), allModules)
val compilerResultWithDCE = compileWasm(
allModules = allModules,
backendContext = backendContext,
baseFileName = baseFileName,
emitNameSection = true,
allowIncompleteImplementations = true,
generateWat = generateWat,
)
return BinaryArtifacts.Wasm(
compilerResult,
compilerResultWithDCE
)
}
override fun shouldRunAnalysis(module: TestModule): Boolean {
return WasmEnvironmentConfigurator.isMainModule(module, testServices)
}
}
fun extractTestPackage(testServices: TestServices): String? {
val ktFiles = testServices.moduleStructure.modules.flatMap { module ->
module.files
.filter { it.isKtFile }
.map {
val project = testServices.compilerConfigurationProvider.getProject(module)
testServices.sourceFileProvider.getKtFileForSourceFile(it, project)
}
}
val fileWithBoxFunction = ktFiles.find { file ->
file.declarations.find { it is KtNamedFunction && it.name == "box" } != null
} ?: return null
return fileWithBoxFunction.packageFqName.asString().takeIf { it.isNotEmpty() }
}
@@ -0,0 +1,19 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test.handlers
import org.jetbrains.kotlin.test.backend.handlers.WasmBinaryArtifactHandler
import org.jetbrains.kotlin.test.model.BinaryArtifacts
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
abstract class AbstractWasmArtifactsCollector(testServices: TestServices) : WasmBinaryArtifactHandler(testServices) {
val modulesToArtifact = mutableMapOf<TestModule, BinaryArtifacts.Wasm>()
override fun processModule(module: TestModule, info: BinaryArtifacts.Wasm) {
modulesToArtifact[module] = info
}
}
@@ -0,0 +1,255 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.wasm.test.handlers
import org.jetbrains.kotlin.backend.wasm.WasmCompilerResult
import org.jetbrains.kotlin.backend.wasm.writeCompilationResult
import org.jetbrains.kotlin.js.JavaScript
import org.jetbrains.kotlin.test.DebugMode
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.jetbrains.kotlin.test.directives.WasmEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.directives.WasmEnvironmentConfigurationDirectives.RUN_UNIT_TESTS
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.moduleStructure
import org.jetbrains.kotlin.wasm.test.tools.WasmVM
import java.io.File
class WasmBoxRunner(
testServices: TestServices
) : AbstractWasmArtifactsCollector(testServices) {
override fun processAfterAllModules(someAssertionWasFailed: Boolean) {
if (!someAssertionWasFailed) {
runWasmCode()
}
}
private fun runWasmCode() {
val artifacts = modulesToArtifact.values.single()
val baseFileName = "index"
val outputDirBase = testServices.getWasmTestOutputDirectory()
val jsFiles = mutableListOf<AdditionalFile>()
val mjsFiles = mutableListOf<AdditionalFile>()
var entryMjs: String? = "test.mjs"
testServices.moduleStructure.modules.forEach { m ->
m.files.forEach { file: TestFile ->
val name = file.name
when {
name.endsWith(".js") ->
jsFiles += AdditionalFile(file.name, file.originalContent)
name.endsWith(".mjs") -> {
mjsFiles += AdditionalFile(file.name, file.originalContent)
if (name == "entry.mjs") {
entryMjs = name
}
}
}
}
}
val originalFile = testServices.moduleStructure.originalTestDataFiles.first()
originalFile.parentFile.resolve(originalFile.nameWithoutExtension + JavaScript.DOT_EXTENSION)
.takeIf { it.exists() }
?.let {
jsFiles += AdditionalFile(it.name, it.readText())
}
originalFile.parentFile.resolve(originalFile.nameWithoutExtension + JavaScript.DOT_MODULE_EXTENSION)
.takeIf { it.exists() }
?.let {
mjsFiles += AdditionalFile(it.name, it.readText())
}
val debugMode = DebugMode.fromSystemProperty("kotlin.js.debugMode")
val startUnitTests = RUN_UNIT_TESTS in testServices.moduleStructure.allDirectives
val testJsQuiet = """
let actualResult;
try {
// Use "dynamic import" to catch exception happened during JS & Wasm modules initialization
let jsModule = await import('./index.mjs');
let wasmExports = jsModule.default;
${if (startUnitTests) "wasmExports.startUnitTests();" else ""}
actualResult = wasmExports.box();
} catch(e) {
console.log('Failed with exception!')
console.log('Message: ' + e.message)
console.log('Name: ' + e.name)
console.log('Stack:')
console.log(e.stack)
}
if (actualResult !== "OK")
throw `Wrong box result '${'$'}{actualResult}'; Expected "OK"`;
""".trimIndent()
val testJsVerbose = testJsQuiet + """
console.log('test passed');
""".trimIndent()
val testJs = if (debugMode >= DebugMode.DEBUG) testJsVerbose else testJsQuiet
fun checkExpectedOutputSize(testFileContent: String, testDir: File) {
val expectedSizes =
InTextDirectivesUtils.findListWithPrefixes(testFileContent, "// WASM_DCE_EXPECTED_OUTPUT_SIZE: ")
.map {
val i = it.indexOf(' ')
val extension = it.substring(0, i)
val size = it.substring(i + 1)
extension.trim().lowercase() to size.filter(Char::isDigit).toInt()
}
val filesByExtension = testDir.listFiles()?.groupBy { it.extension }.orEmpty()
val errors = expectedSizes.mapNotNull { (extension, expectedSize) ->
val totalSize = filesByExtension[extension].orEmpty().sumOf { it.length() }
val thresholdPercent = 1
val thresholdInBytes = expectedSize * thresholdPercent / 100
val expectedMinSize = expectedSize - thresholdInBytes
val expectedMaxSize = expectedSize + thresholdInBytes
val diff = totalSize - expectedSize
val message = "Total size of $extension files is $totalSize," +
" but expected $expectedSize$thresholdInBytes [$expectedMinSize .. $expectedMaxSize]." +
" Diff: $diff (${diff * 100 / expectedSize}%)"
if (debugMode >= DebugMode.DEBUG) {
println(" ------ $message")
}
if (totalSize !in expectedMinSize..expectedMaxSize) message else null
}
if (errors.isNotEmpty()) throw AssertionError(errors.joinToString("\n"))
}
fun writeToFilesAndRunTest(mode: String, res: WasmCompilerResult) {
val dir = File(outputDirBase, mode)
dir.mkdirs()
writeCompilationResult(res, dir, baseFileName)
File(dir, "test.mjs").writeText(testJs)
for (mjsFile: AdditionalFile in mjsFiles) {
File(dir, mjsFile.name).writeText(mjsFile.content)
}
val jsFilePaths = mutableListOf<String>()
for (jsFile: AdditionalFile in jsFiles) {
val file = File(dir, jsFile.name)
file.writeText(jsFile.content)
jsFilePaths += file.canonicalPath
}
if (debugMode >= DebugMode.DEBUG) {
File(dir, "index.html").writeText(
"""
<!DOCTYPE html>
<html lang="en">
<body>
<span id="test">UNKNOWN</span>
<script type="module">
let test = document.getElementById("test")
try {
await import("./test.mjs");
test.style.backgroundColor = "#0f0";
test.textContent = "OK"
} catch(e) {
test.style.backgroundColor = "#f00";
test.textContent = "NOT OK"
throw e;
}
</script>
</body>
</html>
""".trimIndent()
)
val path = dir.absolutePath
println(" ------ $mode Wat file://$path/index.wat")
println(" ------ $mode Wasm file://$path/index.wasm")
println(" ------ $mode JS file://$path/index.uninstantiated.mjs")
println(" ------ $mode JS file://$path/index.mjs")
println(" ------ $mode Test file://$path/test.mjs")
val projectName = "kotlin"
println(" ------ $mode HTML http://0.0.0.0:63342/$projectName/${dir.path}/index.html")
for (mjsFile: AdditionalFile in mjsFiles) {
println(" ------ $mode External ESM file://$path/${mjsFile.name}")
}
}
val testFileText = originalFile.readText()
val failsIn: List<String> = InTextDirectivesUtils.findListWithPrefixes(testFileText, "// WASM_FAILS_IN: ")
val exceptions = listOf(WasmVM.V8, WasmVM.SpiderMonkey).mapNotNull map@{ vm ->
try {
if (debugMode >= DebugMode.DEBUG) {
println(" ------ Run in ${vm.name}" + if (vm.shortName in failsIn) " (expected to fail)" else "")
}
vm.run(
"./${entryMjs}",
jsFilePaths,
workingDirectory = dir
)
if (vm.shortName in failsIn) {
return@map AssertionError("The test expected to fail in ${vm.name}. Please update the testdata.")
}
} catch (e: Throwable) {
if (vm.shortName !in failsIn) {
return@map e
}
}
null
}
when (exceptions.size) {
0 -> {} // Everything OK
1 -> {
throw exceptions.single()
}
else -> {
throw AssertionError("Failed with several exceptions. Look at suppressed exceptions below.").apply {
exceptions.forEach { addSuppressed(it) }
}
}
}
if (mode == "dce") {
checkExpectedOutputSize(testFileText, dir)
}
}
writeToFilesAndRunTest("dev", artifacts.compilerResult)
writeToFilesAndRunTest("dce", artifacts.compilerResultWithDCE)
}
private class AdditionalFile(val name: String, val content: String)
}
fun TestServices.getWasmTestOutputDirectory(): File {
val originalFile = moduleStructure.originalTestDataFiles.first()
val allDirectives = moduleStructure.allDirectives
val pathToRootOutputDir = allDirectives[WasmEnvironmentConfigurationDirectives.PATH_TO_ROOT_OUTPUT_DIR].first()
val testGroupDirPrefix = allDirectives[WasmEnvironmentConfigurationDirectives.TEST_GROUP_OUTPUT_DIR_PREFIX].first()
val pathToTestDir = allDirectives[WasmEnvironmentConfigurationDirectives.PATH_TO_TEST_DIR].first()
val testGroupOutputDir = File(File(pathToRootOutputDir, "out"), testGroupDirPrefix)
val stopFile = File(pathToTestDir)
return generateSequence(originalFile.parentFile) { it.parentFile }
.takeWhile { it != stopFile }
.map { it.name }
.toList().asReversed()
.fold(testGroupOutputDir, ::File)
.let { File(it, originalFile.nameWithoutExtension) }
}