[K/JS] Rework kotlin tests compilation to make it works with per-file granularity ^KT-61525 Fixed

This commit is contained in:
Artem Kobzar
2023-11-16 19:02:04 +00:00
committed by Space Team
parent a4b3b08e59
commit ff50d40b32
48 changed files with 2290 additions and 135 deletions
@@ -47,7 +47,7 @@ interface JsCommonBackendContext : CommonBackendContext {
val enumEntries: IrClassSymbol
val createEnumEntries: IrSimpleFunctionSymbol
fun createTestContainerFun(irFile: IrFile): IrSimpleFunction
fun createTestContainerFun(container: IrDeclaration): IrSimpleFunction
}
@@ -120,14 +120,17 @@ class JsIrBackendContext(
val testFunsPerFile = hashMapOf<IrFile, IrSimpleFunction>()
override fun createTestContainerFun(irFile: IrFile): IrSimpleFunction {
return testFunsPerFile.getOrPut(irFile) {
irFactory.addFunction(irFile) {
name = Name.identifier("test fun")
returnType = irBuiltIns.unitType
origin = JsIrBuilder.SYNTHESIZED_DECLARATION
}.apply {
body = irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET, emptyList())
override fun createTestContainerFun(container: IrDeclaration): IrSimpleFunction {
val irFile = container.file
return irFactory.stageController.restrictTo(container) {
testFunsPerFile.getOrPut(irFile) {
irFactory.addFunction(irFile) {
name = Name.identifier("test fun")
returnType = irBuiltIns.unitType
origin = JsIrBuilder.SYNTHESIZED_DECLARATION
}.apply {
body = irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET, emptyList())
}
}
}
}
@@ -244,4 +244,4 @@ internal fun CrossModuleReferences.crossModuleReferencesHashForIC() = HashCalcul
update(import.moduleExporter.internalName.toString())
}
}
}.finalizeAndGetHash()
}.finalizeAndGetHash()
@@ -30,7 +30,7 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
private fun SrcFileArtifact.loadJsIrModuleHeaders(moduleArtifact: ModuleArtifact) = with(loadJsIrFragments()!!) {
LoadedJsIrModuleHeaders(
mainFragment.mainFunction,
mainFragment.mainFunctionTag,
mainFragment.run {
asIrModuleHeader(
getMainFragmentExternalName(moduleArtifact),
@@ -83,7 +83,10 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
protected open val filePrefix by lazy(LazyThreadSafetyMode.NONE) { fileArtifact.srcFilePath.run { "${substringAfterLast('/')}.${cityHash64()}" } }
override fun loadJsIrModule(): JsIrModule {
val fragments = fileArtifact.loadJsIrFragments()!!
val fragments = fileArtifact.loadJsIrFragments()!!.also {
it.mainFragment.testEnvironment = null
}
val isExportFileCachedInfo = this is ExportFileCachedInfo
return JsIrModule(
jsIrHeader.moduleName,
@@ -97,6 +100,7 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
open class MainFileCachedInfo(moduleArtifact: ModuleArtifact, fileArtifact: SrcFileArtifact, moduleHeader: JsIrModuleHeader? = null) :
SerializableCachedFileInfo(moduleArtifact, fileArtifact, moduleHeader) {
var mainFunctionTag: String? = null
var testEnvironment: JsIrProgramTestEnvironment? = null
var exportFileCachedInfo: ExportFileCachedInfo? = null
val jsFileArtifact by lazy(LazyThreadSafetyMode.NONE) { getArtifactWithName(CACHED_FILE_JS) }
@@ -105,25 +109,27 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
class Merged(private val cachedFileInfos: List<MainFileCachedInfo>) :
MainFileCachedInfo(cachedFileInfos.first().moduleArtifact, cachedFileInfos.first().fileArtifact) {
override fun loadJsIrModule(): JsIrModule = cachedFileInfos.map { it.loadJsIrModule() }.merge()
override val filePrefix by lazy(LazyThreadSafetyMode.NONE) {
val hash = cachedFileInfos.map { it.fileArtifact.srcFilePath }.sorted().joinToString().cityHash64()
fileArtifact.srcFilePath.run { "${substringAfterLast('/')}.$hash.merged" }
}
override fun loadJsIrModule(): JsIrModule = cachedFileInfos.map { it.loadJsIrModule() }.merge()
init {
assert(cachedFileInfos.size > 1) { "Merge is unnecessary" }
val isModified = cachedFileInfos.any { it.fileArtifact.isModified() }
var isModified = false
for (info in cachedFileInfos) {
if (!info.fileArtifact.isModified()) {
isModified = true
}
info.testEnvironment?.let { testEnvironment = it }
}
val mainAndExportHeaders = when {
isModified -> cachedFileInfos.asSequence().map { it.fileArtifact.loadJsIrModuleHeaders(moduleArtifact) }
else -> cachedFileInfos.asSequence().map {
LoadedJsIrModuleHeaders(
it.mainFunctionTag,
it.jsIrHeader,
it.exportFileCachedInfo?.jsIrHeader
)
}
else -> cachedFileInfos.asSequence().map { LoadedJsIrModuleHeaders(it.mainFunctionTag, it.jsIrHeader, it.exportFileCachedInfo?.jsIrHeader) }
}
val mainHeaders = mutableListOf<JsIrModuleHeader>()
@@ -141,15 +147,13 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
}
jsIrHeader = mainHeaders.merge()
exportFileCachedInfo = exportHeaders
.takeIf { it.isNotEmpty() }
?.let {
ExportFileCachedInfo.Merged(
filePrefix,
it.merge(),
cachedFileInfos.mapNotNull(MainFileCachedInfo::exportFileCachedInfo)
)
}
exportFileCachedInfo = exportHeaders.takeIf { it.isNotEmpty() }?.let {
ExportFileCachedInfo.Merged(
filePrefix,
it.merge(),
cachedFileInfos.mapNotNull(MainFileCachedInfo::exportFileCachedInfo)
)
}
}
}
}
@@ -175,6 +179,8 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
class ModuleProxyFileCachedInfo(moduleArtifact: ModuleArtifact, moduleHeader: JsIrModuleHeader? = null) :
CachedFileInfo(moduleArtifact, moduleHeader) {
var mainFunctionTag: String? = null
var suiteFunctionTag: String? = null
var packagesToItsTestFunctions: CachedTestFunctionsWithTheirPackage = emptyMap()
val jsFileArtifact by lazy(LazyThreadSafetyMode.NONE) { getArtifactWithName(CACHED_FILE_JS) }
val dtsFileArtifact by lazy(LazyThreadSafetyMode.NONE) { getArtifactWithName(CACHED_FILE_D_TS) }
@@ -187,6 +193,8 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
jsIrHeader.externalModuleName,
jsIrHeader.externalModuleName,
mainFunctionTag,
suiteFunctionTag,
packagesToItsTestFunctions,
jsIrHeader.importedWithEffectInModuleWithName
)
}
@@ -201,9 +209,20 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
it.crossFileReferencesHash = ICHash.fromProtoStream(this)
if (it is CachedFileInfo.ExportFileCachedInfo) {
it.tsDeclarationsHash = runIf(readBool()) { readInt64() }
reexportedIn = cachedFileInfo.moduleArtifact.moduleExternalName
when (it) {
is CachedFileInfo.MainFileCachedInfo -> {
it.mainFunctionTag = ifTrue { readString() }
it.testEnvironment = ifTrue { JsIrProgramTestEnvironment(readString(), readString()) }
}
is CachedFileInfo.ExportFileCachedInfo -> {
it.tsDeclarationsHash = ifTrue { readInt64() }
reexportedIn = cachedFileInfo.moduleArtifact.moduleExternalName
}
is CachedFileInfo.ModuleProxyFileCachedInfo -> {
it.mainFunctionTag = ifTrue { readString() }
it.suiteFunctionTag = ifTrue { readString() }
it.packagesToItsTestFunctions = loadTestFunctions()
}
}
@@ -222,6 +241,14 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
)
}
private fun CodedInputStream.loadTestFunctions() = buildMap {
repeat(readInt32()) {
put(readString(), buildList {
repeat(readInt32()) { add(readString()) }
})
}
}
private fun <T> CachedFileInfo.MainFileCachedInfo.readModuleHeaderCache(f: CodedInputStream.() -> T): T? =
moduleHeaderArtifact?.useCodedInputIfExists(f)
@@ -231,7 +258,6 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
return mainFileCachedFileInfo.readModuleHeaderCache {
mainFileCachedFileInfo.apply {
exportFileCachedInfo = fetchFileInfoForExportedPart(this)
mainFunctionTag = ifTrue { readString() }
loadSingleCachedFileInfo(this)
}
}
@@ -239,10 +265,7 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
private fun ModuleArtifact.fetchModuleProxyFileInfo(): CachedFileInfo.ModuleProxyFileCachedInfo? {
val mainFileCachedFileInfo = CachedFileInfo.ModuleProxyFileCachedInfo(this)
return mainFileCachedFileInfo.moduleHeaderArtifact?.useCodedInputIfExists {
mainFileCachedFileInfo.mainFunctionTag = ifTrue { readString() }
loadSingleCachedFileInfo(mainFileCachedFileInfo)
}
return mainFileCachedFileInfo.moduleHeaderArtifact?.useCodedInputIfExists { loadSingleCachedFileInfo(mainFileCachedFileInfo) }
}
private fun CodedInputStream.fetchFileInfoForExportedPart(mainCachedFileInfo: CachedFileInfo.MainFileCachedInfo): CachedFileInfo.ExportFileCachedInfo? {
@@ -256,24 +279,43 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
private fun CodedOutputStream.commitSingleFileInfo(cachedFileInfo: CachedFileInfo) {
writeStringNoTag(cachedFileInfo.jsIrHeader.externalModuleName)
cachedFileInfo.crossFileReferencesHash.toProtoStream(this)
if (cachedFileInfo is CachedFileInfo.ExportFileCachedInfo) {
ifNotNull(cachedFileInfo.tsDeclarationsHash, ::writeInt64NoTag)
when (cachedFileInfo) {
is CachedFileInfo.MainFileCachedInfo -> {
ifNotNull(cachedFileInfo.mainFunctionTag, ::writeStringNoTag)
ifNotNull(cachedFileInfo.testEnvironment) {
writeStringNoTag(it.testFunctionTag)
writeStringNoTag(it.suiteFunctionTag)
}
}
is CachedFileInfo.ExportFileCachedInfo -> ifNotNull(cachedFileInfo.tsDeclarationsHash, ::writeInt64NoTag)
is CachedFileInfo.ModuleProxyFileCachedInfo -> {
ifNotNull(cachedFileInfo.mainFunctionTag, ::writeStringNoTag)
ifNotNull(cachedFileInfo.suiteFunctionTag, ::writeStringNoTag)
writeTestFunctions(cachedFileInfo.packagesToItsTestFunctions)
}
}
ifNotNull(cachedFileInfo.jsIrHeader.importedWithEffectInModuleWithName) { writeStringNoTag(it) }
commitJsIrModuleHeaderNames(cachedFileInfo.jsIrHeader)
}
private fun CodedOutputStream.writeTestFunctions(cachedTestFunctionsWithTheirPackage: CachedTestFunctionsWithTheirPackage) {
writeInt32NoTag(cachedTestFunctionsWithTheirPackage.size)
cachedTestFunctionsWithTheirPackage.forEach { (key, value) ->
writeStringNoTag(key)
writeInt32NoTag(value.size)
value.forEach(::writeStringNoTag)
}
}
private fun CachedFileInfo.commitFileInfo() = when (this) {
is CachedFileInfo.MainFileCachedInfo -> {
moduleHeaderArtifact?.useCodedOutput {
ifNotNull(exportFileCachedInfo) { commitSingleFileInfo(it) }
ifNotNull(mainFunctionTag) { writeStringNoTag(it) }
commitSingleFileInfo(this@commitFileInfo)
}
}
is CachedFileInfo.ModuleProxyFileCachedInfo -> {
moduleHeaderArtifact?.useCodedOutput {
ifNotNull(mainFunctionTag) { writeStringNoTag(it) }
commitSingleFileInfo(this@commitFileInfo)
}
}
@@ -282,26 +324,39 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
private fun ModuleArtifact.generateModuleProxyFileCachedInfo(
mainFunctionTag: String?,
suiteFunctionTag: String?,
cachedTestFunctionsWithTheirPackage: CachedTestFunctionsWithTheirPackage,
importedWithEffectInModuleWithName: String? = null
): CachedFileInfo {
val moduleHeader = generateProxyIrModuleWith(
moduleExternalName,
moduleExternalName,
mainFunctionTag,
suiteFunctionTag,
cachedTestFunctionsWithTheirPackage,
importedWithEffectInModuleWithName
).makeModuleHeader()
return CachedFileInfo.ModuleProxyFileCachedInfo(this, moduleHeader)
.also { it.mainFunctionTag = mainFunctionTag }
.also {
it.mainFunctionTag = mainFunctionTag
it.suiteFunctionTag = suiteFunctionTag
it.packagesToItsTestFunctions = cachedTestFunctionsWithTheirPackage
}
}
private fun ModuleArtifact.loadFileInfoFor(fileArtifact: SrcFileArtifact): CachedFileInfo.MainFileCachedInfo {
val headers = fileArtifact.loadJsIrModuleHeaders(this)
val mainFragment =
headers.mainHeader.associatedModule?.fragments?.single() ?: error("Unexpected multiple fragments inside mainHeader")
val mainCachedFileInfo = CachedFileInfo.MainFileCachedInfo(this, fileArtifact, headers.mainHeader)
.apply { mainFunctionTag = headers.mainFunctionTag }
val mainCachedFileInfo = CachedFileInfo.MainFileCachedInfo(this, fileArtifact, headers.mainHeader).apply {
mainFunctionTag = headers.mainFunctionTag
testEnvironment = mainFragment.testEnvironment
mainFragment.testEnvironment = null
}
if (headers.exportHeader != null) {
val tsDeclarationsHash = fileArtifact.loadJsIrFragments()?.exportFragment?.dts?.raw?.cityHash64()
val tsDeclarationsHash = headers.exportHeader.associatedModule?.fragments?.single()?.dts?.raw?.cityHash64()
val cachedExportFileInfo = mainCachedFileInfo.readModuleHeaderCache { fetchFileInfoForExportedPart(mainCachedFileInfo) }
mainCachedFileInfo.exportFileCachedInfo = if (cachedExportFileInfo?.tsDeclarationsHash != tsDeclarationsHash) {
CachedFileInfo.ExportFileCachedInfo(
@@ -351,7 +406,7 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
override val CachedFileInfo.artifactName get() = jsIrHeader.externalModuleName
override val CachedFileInfo.hasEffect get() = jsIrHeader.importedWithEffectInModuleWithName != null
override val CachedFileInfo.hasExport get() = this is CachedFileInfo.MainFileCachedInfo && exportFileCachedInfo != null
override val CachedFileInfo.packageFqn get() = moduleFragmentToExternalName.excludeFileNameFromExternalName(jsIrHeader.moduleName)
override val CachedFileInfo.packageFqn get() = moduleFragmentToExternalName.getPackageFqn(jsIrHeader.moduleName)
override val CachedFileInfo.mainFunction
get() = when (this) {
is CachedFileInfo.MainFileCachedInfo -> mainFunctionTag
@@ -359,6 +414,9 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
else -> error("Unexpected CachedFileInfo type ${this::class.simpleName}")
}
override fun CachedFileInfo.takeTestEnvironmentOwnership() =
(this as CachedFileInfo.MainFileCachedInfo).testEnvironment
override fun SrcFileArtifact.generateArtifact(module: ModuleArtifact) = when {
isModified() -> module.loadFileInfoFor(this)
else -> module.fetchFileInfoFor(this) ?: module.loadFileInfoFor(this)
@@ -370,10 +428,18 @@ class JsPerFileCache(private val moduleArtifacts: List<ModuleArtifact>) : JsMult
else -> CachedFileInfo.MainFileCachedInfo.Merged(map { it as CachedFileInfo.MainFileCachedInfo })
}
override fun ModuleArtifact.generateArtifact(mainFunctionTag: String?, moduleNameForEffects: String?) =
fetchModuleProxyFileInfo()?.takeIf {
it.mainFunction == mainFunctionTag && it.jsIrHeader.importedWithEffectInModuleWithName == moduleNameForEffects
} ?: generateModuleProxyFileCachedInfo(mainFunctionTag, moduleNameForEffects)
override fun ModuleArtifact.generateArtifact(
mainFunctionTag: String?,
suiteFunctionTag: String?,
testFunctions: CachedTestFunctionsWithTheirPackage,
moduleNameForEffects: String?
) = fetchModuleProxyFileInfo()?.takeIf {
it.mainFunctionTag == mainFunctionTag
&& it.jsIrHeader.importedWithEffectInModuleWithName == moduleNameForEffects
&& suiteFunctionTag == it.suiteFunctionTag &&
it.packagesToItsTestFunctions == testFunctions &&
it.jsIrHeader.importedWithEffectInModuleWithName == moduleNameForEffects
} ?: generateModuleProxyFileCachedInfo(mainFunctionTag, suiteFunctionTag, testFunctions, moduleNameForEffects)
}
return perFileGenerator.generatePerFileArtifacts(moduleArtifacts)
@@ -46,7 +46,7 @@ class TestGenerator(val context: JsCommonBackendContext, val groupByPackage: Boo
if (irFile.declarations.isEmpty()) return
ArrayList(irFile.declarations).forEach {
if (it is IrClass) {
generateTestCalls(it) { if (groupByPackage) suiteForPackage(irFile) else context.createTestContainerFun(irFile) }
generateTestCalls(it) { if (groupByPackage) suiteForPackage(it) else context.createTestContainerFun(it) }
}
// TODO top-level functions
@@ -55,8 +55,11 @@ class TestGenerator(val context: JsCommonBackendContext, val groupByPackage: Boo
private val packageSuites = hashMapOf<FqName, IrSimpleFunction>()
private fun suiteForPackage(irFile: IrFile) = packageSuites.getOrPut(irFile.packageFqName) {
context.suiteFun!!.createInvocation(irFile.packageFqName.asString(), context.createTestContainerFun(irFile))
private fun suiteForPackage(container: IrDeclaration): IrSimpleFunction {
val irFile = container.file
return packageSuites.getOrPut(irFile.packageFqName) {
context.suiteFun!!.createInvocation(irFile.packageFqName.asString(), context.createTestContainerFun(container))
}
}
private fun IrSimpleFunctionSymbol.createInvocation(
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.backend.common.serialization.checkIsFunctionInterfac
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.ir.backend.js.*
import org.jetbrains.kotlin.ir.backend.js.export.*
import org.jetbrains.kotlin.ir.backend.js.ic.JsPerFileCache
import org.jetbrains.kotlin.ir.backend.js.lower.JsCodeOutliningLowering
import org.jetbrains.kotlin.ir.backend.js.lower.StaticMembersLowering
import org.jetbrains.kotlin.ir.backend.js.lower.isBuiltInClass
@@ -29,7 +30,6 @@ import org.jetbrains.kotlin.js.sourceMap.SourceMapBuilderConsumer
import org.jetbrains.kotlin.js.util.TextOutputImpl
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.utils.memoryOptimizedMap
import org.jetbrains.kotlin.utils.putToMultiMap
import org.jetbrains.kotlin.utils.addToStdlib.runIf
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
import java.io.File
@@ -51,15 +51,28 @@ val IrModuleFragment.safeName: String
fun generateProxyIrModuleWith(
safeName: String,
externalName: String,
mainFunction: String?,
mainFunctionTag: String?,
suiteFunctionTag: String? = null,
cachedTestFunctionsWithTheirPackage: CachedTestFunctionsWithTheirPackage = emptyMap(),
importedWithEffectInModuleWithName: String? = null
): JsIrModule {
val programFragment = JsIrProgramFragment(safeName, "<proxy-file>").apply {
mainFunction?.let {
this.mainFunction = it
this.nameBindings[it] = JsName("main", true)
mainFunctionTag?.let {
this.mainFunctionTag = it
nameBindings[it] = ReservedJsNames.makeMainFunctionName()
}
cachedTestFunctionsWithTheirPackage.takeIf { it.isNotEmpty() }?.let {
nameBindings += it.values.asSequence()
.flatten()
.map { tag -> tag to ReservedJsNames.makeTestFunctionName() }
.plus(suiteFunctionTag!! to ReservedJsNames.makeSuiteFunctionName())
JsTestFunctionTransformer.generateTestFunctionCall(
it.asTestFunctionContainers(suiteFunctionTag, nameBindings)
)?.run { declarations.statements += makeStmt() }
}
}
return JsIrModule(
safeName,
externalName,
@@ -276,25 +289,35 @@ class IrModuleToJsTransformer(
}
private fun generateJsIrProgramPerFile(exportData: List<IrAndExportedDeclarations>, mode: TranslationMode): JsIrProgram {
val mainModule = exportData.last()
val mainModuleWithExportedData = exportData.last()
val perFileGenerator = object : PerFileGenerator<IrAndExportedDeclarations, IrFileExports, JsIrModules> {
override val mainModuleName get() = mainModule.fragment.safeName
override val mainModuleName = mainModuleWithExportedData.fragment.safeName
private val JsIrModules.mainFragment get() = mainModule.fragments.first()
override val IrAndExportedDeclarations.isMain get() = this === mainModule
override val IrAndExportedDeclarations.isMain get() = this === mainModuleWithExportedData
override val IrAndExportedDeclarations.fileList get() = files
override val JsIrModules.artifactName get() = this.mainModule.externalModuleName
override val JsIrModules.hasEffect get() = this.mainModule.importedWithEffectInModuleWithName != null
override val JsIrModules.hasExport get() = this.exportModule != null
override val JsIrModules.packageFqn get() = this.mainModule.fragments.first().packageFqn
override val JsIrModules.mainFunction get() = this.mainModule.fragments.first().mainFunction
override val JsIrModules.artifactName get() = mainModule.externalModuleName
override val JsIrModules.hasEffect get() = mainModule.importedWithEffectInModuleWithName != null
override val JsIrModules.hasExport get() = exportModule != null
override val JsIrModules.packageFqn get() = mainFragment.packageFqn
override val JsIrModules.mainFunction get() = mainFragment.mainFunctionTag
override fun JsIrModules.takeTestEnvironmentOwnership(): JsIrProgramTestEnvironment? {
val fragment = mainFragment
return fragment.testEnvironment.also { fragment.testEnvironment = null }
}
override fun List<JsIrModules>.merge() =
JsIrModules(map { it.mainModule }.merge(), mapNotNull { it.exportModule }.ifNotEmpty { merge() })
override fun IrAndExportedDeclarations.generateArtifact(mainFunctionTag: String?, moduleNameForEffects: String?) =
JsIrModules(toJsIrProxyModule(mainFunctionTag, moduleNameForEffects))
override fun IrAndExportedDeclarations.generateArtifact(
mainFunctionTag: String?,
suiteFunctionTag: String?,
testFunctions: CachedTestFunctionsWithTheirPackage,
moduleNameForEffects: String?
) = JsIrModules(toJsIrProxyModule(mainFunctionTag, suiteFunctionTag, testFunctions, moduleNameForEffects))
override fun IrFileExports.generateArtifact(module: IrAndExportedDeclarations) = takeIf { !file.couldBeSkipped() }
?.let { generateProgramFragment(it, mode) }
@@ -332,13 +355,17 @@ class IrModuleToJsTransformer(
}
private fun IrAndExportedDeclarations.toJsIrProxyModule(
mainFunction: String?,
mainFunctionTag: String?,
suiteFunctionTag: String?,
cachedTestFunctionsWithTheirPackage: CachedTestFunctionsWithTheirPackage,
importedWithEffectInModuleWithName: String? = null
): JsIrModule {
return generateProxyIrModuleWith(
fragment.safeName,
moduleFragmentToNameMapper.getExternalNameFor(fragment),
mainFunction,
mainFunctionTag,
suiteFunctionTag,
cachedTestFunctionsWithTheirPackage,
importedWithEffectInModuleWithName
)
}
@@ -422,12 +449,6 @@ class IrModuleToJsTransformer(
result.initializers.statements += staticContext.initializerBlock.statements
result.eagerInitializers.statements += staticContext.eagerInitializerBlock.statements
backendContext.testFunsPerFile[fileExports.file]?.let {
result.testFunInvocation = JsInvocation(staticContext.getNameForStaticFunction(it).makeRef()).makeStmt()
result.suiteFn = staticContext.getNameForStaticFunction(backendContext.suiteFun!!.owner)
}
result.importedModules += nameGenerator.importedModules
val definitionSet = fileExports.file.declarations.toSet()
@@ -435,9 +456,16 @@ class IrModuleToJsTransformer(
if (shouldReferMainFunction) {
JsMainFunctionDetector(backendContext).getMainFunctionOrNull(fileExports.file)
?.let { backendContext.mapping.mainFunctionToItsWrapper[it] }
?.let { result.mainFunction = definitionSet.computeTag(it) }
?.let { result.mainFunctionTag = definitionSet.computeTag(it) }
}
backendContext.testFunsPerFile[fileExports.file]
?.let { definitionSet.computeTag(it) }
?.let {
val suiteFunctionTag = definitionSet.computeTag(backendContext.suiteFun!!.owner) ?: error("Expect suite function tag exists")
result.testEnvironment = JsIrProgramTestEnvironment(it, suiteFunctionTag)
}
result.computeAndSaveNameBindings(definitionSet, nameGenerator)
result.computeAndSaveImports(definitionSet, nameGenerator)
result.computeAndSaveDefinitions(definitionSet, fileExports)
@@ -14,6 +14,11 @@ import org.jetbrains.kotlin.serialization.js.ModuleKind
class JsIrProgramFragments(val mainFragment: JsIrProgramFragment, val exportFragment: JsIrProgramFragment? = null)
data class JsIrProgramTestEnvironment(
val testFunctionTag: String,
val suiteFunctionTag: String
)
class JsIrProgramFragment(val name: String, val packageFqn: String) {
val nameBindings = mutableMapOf<String, JsName>()
val optionalCrossModuleImports = hashSetOf<String>()
@@ -25,11 +30,10 @@ class JsIrProgramFragment(val name: String, val packageFqn: String) {
val classes = mutableMapOf<JsName, JsIrIcClassModel>()
val initializers = JsCompositeBlock()
val eagerInitializers = JsCompositeBlock()
var mainFunction: String? = null
var testFunInvocation: JsStatement? = null
var suiteFn: JsName? = null
var mainFunctionTag: String? = null
val definitions = mutableSetOf<String>()
val polyfills = JsCompositeBlock()
var testEnvironment: JsIrProgramTestEnvironment? = null
}
class JsIrModule(
@@ -0,0 +1,72 @@
/*
* 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.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope
import org.jetbrains.kotlin.js.backend.ast.*
object JsTestFunctionTransformer {
fun generateTestFunctionCall(testFunctionContainers: List<TestFunctionContainer>): JsInvocation? {
if (testFunctionContainers.isEmpty()) return null
val testFunBody = JsBlock()
val testFun = JsFunction(emptyScope, testFunBody, "root test fun")
val suiteFunRef = testFunctionContainers.firstNotNullOf { it.suiteFunctionName }.makeRef()
val tests = testFunctionContainers.groupBy({ it.packageFqn }) {
JsInvocation(it.testFunctionName.makeRef()).makeStmt()
} // String -> [IrSimpleFunction]
for ((pkg, testCalls) in tests) {
val pkgTestFun = JsFunction(emptyScope, JsBlock(), "test fun for $pkg")
pkgTestFun.body.statements += testCalls
testFun.body.statements += JsInvocation(suiteFunRef, JsStringLiteral(pkg), JsBooleanLiteral(false), pkgTestFun).makeStmt()
}
return JsInvocation(testFun)
}
class TestFunctionContainer(
val packageFqn: String,
val testFunctionName: JsName,
val suiteFunctionName: JsName
)
}
private fun Map<String, JsName>.getTestFunctionBySignature(signature: String?): JsName {
return get(signature) ?: error("Null test functions should be filtered on a previous step")
}
private fun Map<String, JsName>.getSuiteFunctionBySignature(signature: String?): JsName {
return get(signature) ?: error("A Suite function signature should be present if a test function signature does")
}
fun List<JsIrProgramFragment>.asTestFunctionContainers(): List<JsTestFunctionTransformer.TestFunctionContainer> {
return mapNotNull { fragment ->
fragment.testEnvironment?.let {
JsTestFunctionTransformer.TestFunctionContainer(
fragment.packageFqn,
fragment.nameBindings.getTestFunctionBySignature(it.testFunctionTag),
fragment.nameBindings.getSuiteFunctionBySignature(it.suiteFunctionTag)
)
}
}
}
fun CachedTestFunctionsWithTheirPackage.asTestFunctionContainers(
suiteFunction: String?,
nameBindings: Map<String, JsName>
): List<JsTestFunctionTransformer.TestFunctionContainer> {
return entries.flatMap { (packageFqn, testFunctions) ->
testFunctions.map {
JsTestFunctionTransformer.TestFunctionContainer(
packageFqn,
nameBindings.getTestFunctionBySignature(it),
nameBindings.getSuiteFunctionBySignature(suiteFunction),
)
}
}
}
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.idea.MainFunctionDetector
import org.jetbrains.kotlin.ir.backend.js.utils.JsMainFunctionDetector
import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope
import org.jetbrains.kotlin.js.backend.ast.*
@@ -61,8 +60,6 @@ class Merger(
rename(f.initializers)
rename(f.eagerInitializers)
f.testFunInvocation?.let { rename(it) }
f.suiteFn?.let { f.suiteFn = rename(it) }
}
}
@@ -229,27 +226,14 @@ class Merger(
moduleBody.addWithComment("block: init", initializerBlock.statements)
// Merge test function invocations
if (fragments.any { it.testFunInvocation != null }) {
val testFunBody = JsBlock()
val testFun = JsFunction(emptyScope, testFunBody, "root test fun")
val suiteFunRef = fragments.firstNotNullOf { it.suiteFn }.makeRef()
val tests = fragments.filter { it.testFunInvocation != null }
.groupBy({ it.packageFqn }) { it.testFunInvocation } // String -> [IrSimpleFunction]
for ((pkg, testCalls) in tests) {
val pkgTestFun = JsFunction(emptyScope, JsBlock(), "test fun for $pkg")
pkgTestFun.body.statements += testCalls
testFun.body.statements += JsInvocation(suiteFunRef, JsStringLiteral(pkg), JsBooleanLiteral(false), pkgTestFun).makeStmt()
}
JsTestFunctionTransformer.generateTestFunctionCall(fragments.asTestFunctionContainers())?.let {
moduleBody.startRegion("block: tests")
moduleBody += JsInvocation(testFun).makeStmt()
moduleBody += it.makeStmt()
moduleBody.endRegion()
}
val fragmentWithMainFunction = JsMainFunctionDetector.pickMainFunctionFromCandidates(fragments) {
JsMainFunctionDetector.MainFunctionCandidate(it.packageFqn, it.mainFunction)
JsMainFunctionDetector.MainFunctionCandidate(it.packageFqn, it.mainFunctionTag)
}
val exportStatements = declareAndCallJsExporter() + additionalExports + transitiveJsExport()
@@ -271,7 +255,7 @@ class Merger(
statements += moduleBody
statements.addWithComment("block: exports", exportStatements)
if (generateCallToMain && fragmentWithMainFunction != null) {
val mainFunctionTag = fragmentWithMainFunction.mainFunction ?: error("Expect to have main function signature at this point")
val mainFunctionTag = fragmentWithMainFunction.mainFunctionTag ?: error("Expect to have main function signature at this point")
val mainFunctionName = fragmentWithMainFunction.nameBindings[mainFunctionTag] ?: error("Expect to have name binding for tag $mainFunctionTag")
statements += JsInvocation(mainFunctionName.makeRef()).makeStmt()
}
@@ -42,10 +42,6 @@ class ModuleFragmentToExternalName(private val jsOutputNamesMapping: Map<IrModul
return module.getJsOutputName()
}
fun excludeFileNameFromExternalName(externalName: String): String {
return externalName.substringBeforeLast('/')
}
private fun IrModuleFragment.getJsOutputName(): String {
return jsOutputNamesMapping[this] ?: sanitizeName(safeName)
}
@@ -55,6 +51,17 @@ class ModuleFragmentToExternalName(private val jsOutputNamesMapping: Map<IrModul
return "$prefix${if (prefix.isNotEmpty()) "/" else ""}$fileName"
}
fun getPackageFqn(externalName: String): String {
val endOfModuleNamePart = externalName.indexOf('/')
val startOfFileNamePart = externalName.lastIndexOf('/')
return if (endOfModuleNamePart == startOfFileNamePart) {
""
} else {
externalName.substring(endOfModuleNamePart + 1, startOfFileNamePart)
.replace('/', '.')
}
}
private val IrFile.outputName: String get() = getJsFileName() ?: nameWithoutExtension
private val IrFile.stableFileName: String get() = getFileStableName(outputName, packageFqName.asString())
}
@@ -9,6 +9,8 @@ import org.jetbrains.kotlin.ir.backend.js.utils.JsMainFunctionDetector
import org.jetbrains.kotlin.utils.addToStdlib.runIf
import org.jetbrains.kotlin.utils.putToMultiMap
typealias CachedTestFunctionsWithTheirPackage = Map<String, List<String>>
interface PerFileGenerator<Module, File, Artifact> {
val mainModuleName: String
@@ -21,9 +23,16 @@ interface PerFileGenerator<Module, File, Artifact> {
val Artifact.packageFqn: String
val Artifact.mainFunction: String?
fun Artifact.takeTestEnvironmentOwnership(): JsIrProgramTestEnvironment?
fun List<Artifact>.merge(): Artifact
fun File.generateArtifact(module: Module): Artifact?
fun Module.generateArtifact(mainFunctionTag: String?, moduleNameForEffects: String?): Artifact
fun Module.generateArtifact(
mainFunctionTag: String?,
suiteFunctionTag: String?,
testFunctions: CachedTestFunctionsWithTheirPackage,
moduleNameForEffects: String?
): Artifact
fun generatePerFileArtifacts(modules: List<Module>): List<Artifact> {
var someModuleHasEffect = false
@@ -32,6 +41,8 @@ interface PerFileGenerator<Module, File, Artifact> {
for (module in modules) {
var hasModuleLevelEffect = false
var hasFileWithExportedDeclaration = false
var suiteFunctionTag: String? = null
val testFunctions = mutableMapOf<String, MutableList<String>>()
val artifacts = module.fileList.mapNotNull {
val generatedArtifact = it.generateArtifact(module) ?: return@mapNotNull null
@@ -44,6 +55,11 @@ interface PerFileGenerator<Module, File, Artifact> {
hasModuleLevelEffect = true
}
generatedArtifact.takeTestEnvironmentOwnership()?.let { (testFunction, suiteFunction) ->
testFunctions.putToMultiMap(generatedArtifact.packageFqn, testFunction)
suiteFunctionTag = suiteFunction
}
putToMultiMap(generatedArtifact.artifactName, generatedArtifact)
generatedArtifact
@@ -54,19 +70,21 @@ interface PerFileGenerator<Module, File, Artifact> {
}
val mainFunctionTag = runIf(module.isMain) {
JsMainFunctionDetector
.pickMainFunctionFromCandidates(artifacts) {
JsMainFunctionDetector.MainFunctionCandidate(
it.packageFqn,
it.mainFunction
)
}
?.mainFunction
JsMainFunctionDetector.pickMainFunctionFromCandidates(artifacts) {
JsMainFunctionDetector.MainFunctionCandidate(
it.packageFqn,
it.mainFunction
)
}?.mainFunction
}
if (mainFunctionTag != null || hasFileWithExportedDeclaration || hasModuleLevelEffect || (module.isMain && someModuleHasEffect)) {
val proxyArtifact =
module.generateArtifact(mainFunctionTag, mainModuleName.takeIf { !module.isMain && hasModuleLevelEffect }) ?: continue
if (mainFunctionTag != null || hasFileWithExportedDeclaration || hasModuleLevelEffect || suiteFunctionTag != null || (module.isMain && someModuleHasEffect)) {
val proxyArtifact = module.generateArtifact(
mainFunctionTag,
suiteFunctionTag,
testFunctions,
mainModuleName.takeIf { !module.isMain && hasModuleLevelEffect }
) ?: continue
putToMultiMap(proxyArtifact.artifactName, proxyArtifact)
}
}
@@ -74,4 +92,4 @@ interface PerFileGenerator<Module, File, Artifact> {
return nameToModulePerFile.values.map { it.merge() }
}
}
}
@@ -13,5 +13,8 @@ class ReservedJsNames {
fun makeInternalModuleName() = JsName("_", false)
fun makeJsExporterName() = JsName("\$jsExportAll\$", false)
fun makeCrossModuleNameRef(moduleName: JsName) = JsNameRef("\$_\$", moduleName.makeRef())
fun makeMainFunctionName() = JsName("main", true)
fun makeTestFunctionName() = JsName("test", true)
fun makeSuiteFunctionName() = JsName("suite", true)
}
}
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.ir.backend.js.export.TypeScriptFragment
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsIrIcClassModel
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsIrProgramFragment
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsIrProgramFragments
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsIrProgramTestEnvironment
import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.backend.ast.metadata.*
@@ -103,10 +104,9 @@ private class JsIrAstDeserializer(private val source: ByteArray) {
readRepeated { optionalCrossModuleImports.add(stringTable[readInt()]) }
readRepeated { classes[nameTable[readInt()]] = readIrIcClassModel() }
ifTrue { testFunInvocation = readStatement() }
ifTrue { mainFunction = readString() }
ifTrue { mainFunctionTag = readString() }
ifTrue { testEnvironment = readTestEnvironment() }
ifTrue { dts = TypeScriptFragment(readString()) }
ifTrue { suiteFn = nameTable[readInt()] }
readRepeated { definitions += stringTable[readInt()] }
}
@@ -119,6 +119,10 @@ private class JsIrAstDeserializer(private val source: ByteArray) {
}
}
private fun readTestEnvironment(): JsIrProgramTestEnvironment {
return JsIrProgramTestEnvironment(stringTable[readInt()], stringTable[readInt()])
}
private fun readStatement(): JsStatement {
return withComments {
withLocation {
@@ -151,22 +151,19 @@ private class JsIrAstSerializer {
writeIrIcModel(model)
}
ifNotNull(fragment.testFunInvocation) {
writeStatement(it)
ifNotNull(fragment.mainFunctionTag) {
writeString(it)
}
ifNotNull(fragment.mainFunction) {
writeString(it)
ifNotNull(fragment.testEnvironment) {
writeInt(internalizeString(it.testFunctionTag))
writeInt(internalizeString(it.suiteFunctionTag))
}
ifNotNull(fragment.dts) {
writeString(it.raw)
}
ifNotNull(fragment.suiteFn) {
writeInt(internalizeName(it))
}
writeCollection(fragment.definitions) {
writeInt(internalizeString(it))
}
@@ -28,6 +28,7 @@ import org.jetbrains.kotlin.ir.types.IrTypeSystemContext
import org.jetbrains.kotlin.ir.types.IrTypeSystemContextImpl
import org.jetbrains.kotlin.ir.util.SymbolTable
import org.jetbrains.kotlin.ir.util.addChild
import org.jetbrains.kotlin.ir.util.file
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
@@ -182,7 +183,8 @@ class WasmBackendContext(
val testEntryPoints: Collection<IrSimpleFunction>
get() = testContainerFuns.values
override fun createTestContainerFun(irFile: IrFile): IrSimpleFunction {
override fun createTestContainerFun(container: IrDeclaration): IrSimpleFunction {
val irFile = container.file
val module = irFile.module
return testContainerFuns.getOrPut(module) {
val file = syntheticFile("tests", module)