JS stdlib api test: various changes
- Setting -Doverwrite.output=true updates gold data - Big packages don't get split into chunks - Unique lines in the API are marked with /*∆*/ and diff test is removed - Annotations on separate lines and other dump format tweaks - Test data moved to libraries/stdlib/api/ - Minor visibility fix to Enum members
This commit is contained in:
committed by
Anton Bannykh
parent
7b61bf9178
commit
cbabb4f76a
@@ -131,6 +131,8 @@ fun Test.setUpBoxTests() {
|
||||
systemProperty("kotlin.ant.launcher.class", "org.apache.tools.ant.Main")
|
||||
}
|
||||
|
||||
systemProperty("overwrite.output", findProperty("overwrite.output") ?: "false")
|
||||
|
||||
val prefixForPpropertiesToForward = "fd."
|
||||
for ((key, value) in properties) {
|
||||
if (key.startsWith(prefixForPpropertiesToForward)) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.jetbrains.kotlin.js.test
|
||||
|
||||
import junit.framework.TestCase
|
||||
import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport
|
||||
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
|
||||
import org.jetbrains.kotlin.cli.js.messageCollectorLogger
|
||||
@@ -13,156 +14,116 @@ import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.descriptors.impl.EnumEntrySyntheticClassDescriptor
|
||||
import org.jetbrains.kotlin.ir.backend.js.MainModule
|
||||
import org.jetbrains.kotlin.ir.backend.js.jsResolveLibraries
|
||||
import org.jetbrains.kotlin.ir.backend.js.loadIr
|
||||
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
|
||||
import org.jetbrains.kotlin.js.config.JsConfig
|
||||
import org.jetbrains.kotlin.jvm.compiler.ExpectedLoadErrorsUtil
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.renderer.*
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
import org.jetbrains.kotlin.resolve.MemberComparator
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.isEffectivelyPublicApi
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.module
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import org.jetbrains.kotlin.test.KotlinTestWithEnvironment
|
||||
import org.jetbrains.kotlin.test.util.RecursiveDescriptorComparator
|
||||
import org.jetbrains.kotlin.test.util.RecursiveDescriptorComparator.RECURSIVE_ALL
|
||||
import java.io.File
|
||||
|
||||
// use -Poverwrite.output=true or -Pfd.overwrite.output=true
|
||||
private val OVERWRITE_EXPECTED_OUTPUT = System.getProperty("overwrite.output")?.toBoolean() ?: false
|
||||
|
||||
class SafeguardTest : TestCase() {
|
||||
|
||||
fun testOutputNotOverwritten() {
|
||||
assertFalse(
|
||||
"Attention! Expected output is being overwritten! Please set OVERWRITE_EXPECTED_OUTPUT to false.",
|
||||
OVERWRITE_EXPECTED_OUTPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ApiTest : KotlinTestWithEnvironment() {
|
||||
|
||||
private val STDLIB_PATH = "js/js.translator/testData/api/stdlib"
|
||||
|
||||
fun testStdlib() {
|
||||
val project = environment.project
|
||||
val configuration = environment.configuration
|
||||
|
||||
configuration.put(CommonConfigurationKeys.MODULE_NAME, "test")
|
||||
configuration.put(JSConfigurationKeys.LIBRARIES, JsConfig.JS_STDLIB)
|
||||
|
||||
val config = JsConfig(project, configuration)
|
||||
|
||||
config.moduleDescriptors.single().checkRecursively(STDLIB_PATH)
|
||||
stdlibModuleApi.markUniqueLinesComparedTo(irStdlibModuleApi).checkRecursively("libraries/stdlib/api/js-v1")
|
||||
}
|
||||
|
||||
private val STDLIB_IR_PATH = "js/js.translator/testData/api/stdlib-ir"
|
||||
|
||||
fun testIrStdlib() {
|
||||
val fullRuntimeKlib: String = System.getProperty("kotlin.js.full.stdlib.path")
|
||||
|
||||
val resolvedLibraries =
|
||||
jsResolveLibraries(listOf(File(fullRuntimeKlib).absolutePath), messageCollectorLogger(MessageCollector.NONE))
|
||||
|
||||
val project = environment.project
|
||||
val configuration = environment.configuration
|
||||
|
||||
val moduleDescriptor = loadIr(
|
||||
project,
|
||||
MainModule.Klib(resolvedLibraries.getFullList().single()),
|
||||
AnalyzerWithCompilerReport(configuration),
|
||||
configuration,
|
||||
resolvedLibraries,
|
||||
listOf()
|
||||
).module.descriptor
|
||||
|
||||
moduleDescriptor.checkRecursively(STDLIB_IR_PATH)
|
||||
irStdlibModuleApi.markUniqueLinesComparedTo(stdlibModuleApi).checkRecursively("libraries/stdlib/api/js")
|
||||
}
|
||||
|
||||
private val STDLIB_DIFF_PATH = "js/js.translator/testData/api/stdlib-diff"
|
||||
private val onlyInStdlib = setOf(
|
||||
"org.khronos.webgl",
|
||||
"org.w3c.css.masking",
|
||||
"org.w3c.dom",
|
||||
"org.w3c.dom.clipboard",
|
||||
"org.w3c.dom.css",
|
||||
"org.w3c.dom.encryptedmedia",
|
||||
"org.w3c.dom.events",
|
||||
"org.w3c.dom.mediacapture",
|
||||
"org.w3c.dom.mediasource",
|
||||
"org.w3c.dom.parsing",
|
||||
"org.w3c.dom.pointerevents",
|
||||
"org.w3c.dom.svg",
|
||||
"org.w3c.dom.url",
|
||||
"org.w3c.fetch",
|
||||
"org.w3c.files",
|
||||
"org.w3c.notifications",
|
||||
"org.w3c.performance",
|
||||
"org.w3c.workers",
|
||||
"org.w3c.xhr"
|
||||
)
|
||||
private val onlyInStdlibIr = setOf("testUtils")
|
||||
private val stdlibModuleApi: Map<FqName, String>
|
||||
get() {
|
||||
val project = environment.project
|
||||
val configuration = environment.configuration
|
||||
|
||||
fun testApiDifference() {
|
||||
val files = STDLIB_PATH.listFiles()
|
||||
val irFiles = STDLIB_IR_PATH.listFiles()
|
||||
configuration.put(CommonConfigurationKeys.MODULE_NAME, "test")
|
||||
configuration.put(JSConfigurationKeys.LIBRARIES, JsConfig.JS_STDLIB)
|
||||
|
||||
val allNames = (files + irFiles).map { it.name.dropLast(3).split('-').first() }.toSet()
|
||||
val config = JsConfig(project, configuration)
|
||||
|
||||
for (name in allNames) {
|
||||
val a = STDLIB_PATH.readFileText(name)
|
||||
val b = STDLIB_IR_PATH.readFileText(name)
|
||||
|
||||
if (a == null) {
|
||||
assertTrue("Package '$name' unexpectedly only present in IR stdlib", name in onlyInStdlibIr)
|
||||
} else if (b == null) {
|
||||
assertTrue("Package '$name' unexpectedly only present in old stdlib", name in onlyInStdlib)
|
||||
} else {
|
||||
val d = diff(a, b)
|
||||
|
||||
if (d.isNotBlank()) {
|
||||
// Uncomment to overwrite the test data
|
||||
// File("$STDLIB_DIFF_PATH/$name.kt").writeText(d)
|
||||
KotlinTestUtils.assertEqualsToFile(File("$STDLIB_DIFF_PATH/$name.kt"), d)
|
||||
}
|
||||
}
|
||||
return config.moduleDescriptors.single().packagesSerialized()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.readFileText(name: String): String? {
|
||||
val f = File("$this/$name.kt")
|
||||
if (f.exists() && f.isFile) {
|
||||
return f.readText()
|
||||
} else {
|
||||
var i = 0
|
||||
var result: String? = null
|
||||
while (true) {
|
||||
val f = File("$this/$name-$i.kt")
|
||||
if (!f.exists() || !f.isFile) break;
|
||||
private val irStdlibModuleApi: Map<FqName, String>
|
||||
get() {
|
||||
val fullRuntimeKlib: String = System.getProperty("kotlin.js.full.stdlib.path")
|
||||
|
||||
result = (result ?: "") + f.readText()
|
||||
val resolvedLibraries =
|
||||
jsResolveLibraries(listOf(File(fullRuntimeKlib).absolutePath), messageCollectorLogger(MessageCollector.NONE))
|
||||
|
||||
++i
|
||||
}
|
||||
val project = environment.project
|
||||
val configuration = environment.configuration
|
||||
|
||||
return result
|
||||
return loadIr(
|
||||
project,
|
||||
MainModule.Klib(resolvedLibraries.getFullList().single()),
|
||||
AnalyzerWithCompilerReport(configuration),
|
||||
configuration,
|
||||
resolvedLibraries,
|
||||
listOf()
|
||||
).module.descriptor.packagesSerialized()
|
||||
}
|
||||
|
||||
private fun Map<FqName, String>.markUniqueLinesComparedTo(other: Map<FqName, String>): Map<FqName, String> {
|
||||
return entries.map { (fqName, api) ->
|
||||
val otherApiLines = other[fqName]?.lines() ?: emptyList()
|
||||
val augmentedApi = diff(api.lines(), otherApiLines)
|
||||
|
||||
fqName to augmentedApi
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun diff(a: String, b: String): String {
|
||||
val aLines = a.lines()
|
||||
val bLines = b.lines()
|
||||
private fun diff(aLines: List<String>, bLines: List<String>): String {
|
||||
val d = Array(aLines.size + 1) { ByteArray(bLines.size + 1) }
|
||||
val c = Array(aLines.size + 1) { ShortArray(bLines.size + 1) }
|
||||
|
||||
val dx = Array(aLines.size + 1) { IntArray(bLines.size + 1) }
|
||||
val dy = Array(aLines.size + 1) { IntArray(bLines.size + 1)}
|
||||
val c = Array(aLines.size + 1) { IntArray(bLines.size + 1)}
|
||||
val DX = 1.toByte()
|
||||
val DY = 2.toByte()
|
||||
val DXY = 3.toByte()
|
||||
|
||||
for (i in 0..aLines.size) {
|
||||
c[i][0] = i
|
||||
dx[i][0] = -1
|
||||
c[i][0] = i.toShort()
|
||||
d[i][0] = DX
|
||||
}
|
||||
for (j in 0..bLines.size) {
|
||||
c[0][j] = j
|
||||
dy[0][j] = -1
|
||||
c[0][j] = j.toShort()
|
||||
d[0][j] = DY
|
||||
}
|
||||
for (i in 1..aLines.size) {
|
||||
for (j in 1..bLines.size) {
|
||||
if (c[i - 1][j] <= c[i][j - 1]) {
|
||||
c[i][j] = c[i - 1][j] + 1
|
||||
dx[i][j] = -1
|
||||
c[i][j] = (c[i - 1][j] + 1).toShort()
|
||||
d[i][j] = DX
|
||||
} else {
|
||||
c[i][j] = c[i][j - 1] + 1
|
||||
dy[i][j] = -1
|
||||
c[i][j] = (c[i][j - 1] + 1).toShort()
|
||||
d[i][j] = DY
|
||||
}
|
||||
if (aLines[i - 1] == bLines[j - 1] && c[i - 1][j - 1] < c[i][j]) {
|
||||
c[i][j] = c[i - 1][j - 1]
|
||||
dx[i][j] = -1
|
||||
dy[i][j] = -1
|
||||
d[i][j] = DXY
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,32 +132,20 @@ class ApiTest : KotlinTestWithEnvironment() {
|
||||
|
||||
var x = aLines.size
|
||||
var y = bLines.size
|
||||
var hasDiff = false
|
||||
|
||||
while (x != 0 && y != 0) {
|
||||
val tdx = dx[x][y]
|
||||
val tdy = dy[x][y]
|
||||
while (x != 0 || y != 0) {
|
||||
val tdx = if ((d[x][y].toInt() and DX.toInt()) == 0) 0 else -1
|
||||
val tdy = if ((d[x][y].toInt() and DY.toInt()) == 0) 0 else -1
|
||||
|
||||
if (tdx != 0) {
|
||||
if (tdy != 0) {
|
||||
if (hasDiff) {
|
||||
result += "--- ${x + 1},${y + 1} ---"
|
||||
hasDiff = false
|
||||
}
|
||||
} else {
|
||||
result += "- ${aLines[x - 1]}"
|
||||
hasDiff = true
|
||||
}
|
||||
} else if (tdy != 0) {
|
||||
result += "+ ${bLines[y - 1]}"
|
||||
hasDiff = true
|
||||
result += (if (tdy == 0) "/*∆*/ " else "") + aLines[x - 1]
|
||||
}
|
||||
|
||||
x += tdx
|
||||
y += tdy
|
||||
}
|
||||
|
||||
return result.reversed().joinToString("\n", postfix = "\n")
|
||||
return result.reversed().joinToString("\n")
|
||||
}
|
||||
|
||||
private fun String.listFiles(): Array<File> {
|
||||
@@ -206,27 +155,33 @@ class ApiTest : KotlinTestWithEnvironment() {
|
||||
return dirFile.listFiles()!!
|
||||
}
|
||||
|
||||
private fun ModuleDescriptor.checkRecursively(dir: String) {
|
||||
val dirFile = File(dir)
|
||||
assertTrue("Directory does not exist: ${dirFile.absolutePath}", dirFile.exists())
|
||||
assertTrue("Not a directory: ${dirFile.absolutePath}", dirFile.isDirectory)
|
||||
val files = dirFile.listFiles()!!.map { it.name }.toMutableSet()
|
||||
allPackages().forEach { fqName ->
|
||||
getPackage(fqName).serialize()?.let { serialized ->
|
||||
serialized.forEachIndexed { index, part ->
|
||||
private fun String.cleanDir() {
|
||||
listFiles().forEach { it.delete() }
|
||||
}
|
||||
|
||||
val fileName =
|
||||
(if (fqName.isRoot) "ROOT" else fqName.asString()) + (if (serialized.size == 1) "" else "-$index") + ".kt"
|
||||
files -= fileName
|
||||
private fun Map<FqName, String>.checkRecursively(dir: String) {
|
||||
|
||||
// Uncomment to overwrite the test data
|
||||
// File("$dir/$fileName").writeText(part)
|
||||
KotlinTestUtils.assertEqualsToFile(File("$dir/$fileName"), part)
|
||||
}
|
||||
}
|
||||
|
||||
if (OVERWRITE_EXPECTED_OUTPUT) {
|
||||
dir.cleanDir()
|
||||
}
|
||||
|
||||
assertTrue("Extra files found: ${files}", files.isEmpty())
|
||||
val files = dir.listFiles().map { it.name }.toMutableSet()
|
||||
entries.forEach { (fqName, serialized) ->
|
||||
val fileName = (if (fqName.isRoot) "ROOT" else fqName.asString()) + ".kt"
|
||||
files -= fileName
|
||||
|
||||
if (OVERWRITE_EXPECTED_OUTPUT) {
|
||||
File("$dir/$fileName").writeText(serialized)
|
||||
}
|
||||
KotlinTestUtils.assertEqualsToFile(File("$dir/$fileName"), serialized)
|
||||
}
|
||||
|
||||
assertTrue("Extra files found: $files", files.isEmpty())
|
||||
}
|
||||
|
||||
private fun ModuleDescriptor.packagesSerialized(): Map<FqName, String> {
|
||||
return allPackages().mapNotNull { fqName -> getPackage(fqName).serialize()?.let { fqName to it } }.toMap()
|
||||
}
|
||||
|
||||
private fun ModuleDescriptor.allPackages(): Collection<FqName> {
|
||||
@@ -243,50 +198,12 @@ class ApiTest : KotlinTestWithEnvironment() {
|
||||
return result
|
||||
}
|
||||
|
||||
private fun PackageViewDescriptor.serialize(): List<String>? {
|
||||
val comparator = RecursiveDescriptorComparator(RECURSIVE_ALL.filterRecursion {
|
||||
when {
|
||||
it is MemberDescriptor && it.isExpect -> false
|
||||
it is DeclarationDescriptorWithVisibility && !it.visibility.isPublicAPI -> false
|
||||
it is CallableMemberDescriptor && !it.kind.isReal -> false
|
||||
it is PackageViewDescriptor -> false
|
||||
else -> true
|
||||
}
|
||||
}.renderDeclarationsFromOtherModules(true))
|
||||
private fun PackageViewDescriptor.serialize(): String? {
|
||||
val serialized = ModuleDescriptorApiGenerator.generate(this).trim()
|
||||
|
||||
val serialized = comparator.serializeRecursively(this).trim()
|
||||
if (serialized.count { it == '\n' } <= 1) return null
|
||||
|
||||
val lines = serialized.lines()
|
||||
|
||||
if (lines.size <= 1) return null
|
||||
|
||||
if (lines.size > LINES_PER_FILE_CUTOFF) {
|
||||
val result = mutableListOf<String>()
|
||||
|
||||
val sb = StringBuilder()
|
||||
var cnt = 0
|
||||
var bracketBalance = 0
|
||||
|
||||
for (d in lines) {
|
||||
if (cnt > LINES_PER_FILE_CUTOFF && bracketBalance == 0 && (d.isBlank() || !d[0].isWhitespace())) {
|
||||
result += sb.toString()
|
||||
sb.clear()
|
||||
cnt = 0
|
||||
}
|
||||
|
||||
sb.append(d).append('\n')
|
||||
++cnt
|
||||
bracketBalance += d.count { it == '{' } - d.count { it == '}' }
|
||||
}
|
||||
|
||||
if (sb.isNotBlank()) {
|
||||
result += sb.toString()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return listOf(serialized)
|
||||
return serialized
|
||||
}
|
||||
|
||||
override fun createEnvironment(): KotlinCoreEnvironment {
|
||||
@@ -294,5 +211,92 @@ class ApiTest : KotlinTestWithEnvironment() {
|
||||
}
|
||||
}
|
||||
|
||||
// IDEA isn't able to show diff for files that are too big
|
||||
private val LINES_PER_FILE_CUTOFF = 1000
|
||||
private val Renderer = DescriptorRenderer.withOptions {
|
||||
withDefinedIn = false
|
||||
excludedAnnotationClasses = setOf(FqName(ExpectedLoadErrorsUtil.ANNOTATION_CLASS_NAME))
|
||||
overrideRenderingPolicy = OverrideRenderingPolicy.RENDER_OPEN_OVERRIDE
|
||||
includePropertyConstant = true
|
||||
classifierNamePolicy = ClassifierNamePolicy.FULLY_QUALIFIED
|
||||
annotationArgumentsRenderingPolicy = AnnotationArgumentsRenderingPolicy.UNLESS_EMPTY
|
||||
modifiers = DescriptorRendererModifier.ALL
|
||||
actualPropertiesInPrimaryConstructor = true
|
||||
alwaysRenderModifiers = true
|
||||
eachAnnotationOnNewLine = true
|
||||
includePropertyConstant = true
|
||||
normalizedVisibilities = true
|
||||
parameterNameRenderingPolicy = ParameterNameRenderingPolicy.ALL
|
||||
renderCompanionObjectName = true
|
||||
renderDefaultModality = true
|
||||
renderDefaultVisibility = true
|
||||
renderConstructorKeyword = true
|
||||
}
|
||||
|
||||
// Inspired by https://github.com/kotlin/kotlinx.team.infra/blob/723a489eaf978603362acfae8f76fd3fb3c21bfa/main/src/kotlinx/team/infra/api/ModuleDescriptorApiGenerator.kt
|
||||
private object ModuleDescriptorApiGenerator {
|
||||
|
||||
fun generate(packageView: PackageViewDescriptor): String {
|
||||
return buildString {
|
||||
val fragments = packageView.fragments.filter { it.fqName == packageView.fqName }
|
||||
val entities = fragments.flatMap { DescriptorUtils.getAllDescriptors(it.getMemberScope()) }
|
||||
|
||||
appendEntities("", entities)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Appendable.appendEntities(indent: String, entities: Iterable<DeclarationDescriptor>) {
|
||||
entities
|
||||
.asSequence()
|
||||
.filter { it is MemberDescriptor && it.isEffectivelyPublicApi }
|
||||
.filter { it !is MemberDescriptor || !it.isExpect }
|
||||
.filter { it !is CallableMemberDescriptor || it.kind.isReal }
|
||||
.sortedWith(MemberComparator.INSTANCE)
|
||||
.forEachIndexed { i, descriptor ->
|
||||
if (i != 0) appendLine()
|
||||
when (descriptor) {
|
||||
is ClassDescriptor -> appendClass(indent, descriptor)
|
||||
is PropertyDescriptor -> appendProperty(indent, descriptor)
|
||||
else -> render(indent, descriptor).appendLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Appendable.render(indent: String, descriptor: DeclarationDescriptor): Appendable {
|
||||
Renderer.render(descriptor).lines().forEachIndexed { i, line ->
|
||||
if (i != 0) appendLine()
|
||||
append("$indent$line")
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun Appendable.appendClass(indent: String, descriptor: ClassDescriptor) {
|
||||
render(indent, descriptor)
|
||||
|
||||
if (descriptor is EnumEntrySyntheticClassDescriptor) {
|
||||
appendLine()
|
||||
return
|
||||
}
|
||||
|
||||
appendLine(" {")
|
||||
|
||||
val members = DescriptorUtils.getAllDescriptors(descriptor.unsubstitutedMemberScope) + descriptor.constructors
|
||||
appendEntities("$indent ", members)
|
||||
|
||||
appendLine("$indent}")
|
||||
}
|
||||
|
||||
private fun Appendable.appendProperty(indent: String, descriptor: PropertyDescriptor) {
|
||||
render(indent, descriptor)
|
||||
|
||||
val hasGetter = descriptor.getter?.isEffectivelyPublicApi ?: false
|
||||
val hasSetter = descriptor.setter?.isEffectivelyPublicApi ?: false
|
||||
|
||||
if (hasGetter || hasSetter) {
|
||||
append(" {")
|
||||
if (hasGetter) append(" get;")
|
||||
if (hasSetter) append(" set;")
|
||||
append(" }")
|
||||
}
|
||||
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user