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:
Anton Bannykh
2020-06-06 16:19:04 +03:00
committed by Anton Bannykh
parent 7b61bf9178
commit cbabb4f76a
157 changed files with 54588 additions and 34221 deletions
+2
View File
@@ -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()
}
}