Move code conformance tests from compiler tests

#KTI-962
This commit is contained in:
Bogdan Mukvich
2022-11-03 11:49:51 +01:00
committed by Space Team
parent dec0946896
commit 080b034950
7 changed files with 37 additions and 19 deletions
@@ -0,0 +1,410 @@
/*
* Copyright 2010-2022 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.code
import com.intellij.openapi.util.io.FileUtil
import junit.framework.TestCase
import org.jetbrains.kotlin.config.LanguageFeature
import java.io.File
import java.util.*
import java.util.regex.Pattern
import kotlin.collections.HashSet
class CodeConformanceTest : TestCase() {
companion object {
private val JAVA_FILE_PATTERN = Pattern.compile(".+\\.java")
private val SOURCES_FILE_PATTERN = Pattern.compile(".+\\.(java|kt|js)")
private const val MAX_STEPS_COUNT = 100
private val nonSourcesMatcher = FileMatcher(
File("."),
listOf(
".git",
".idea",
"build/js",
"build/tmp",
"buildSrc",
"compiler/build",
"compiler/fir/lightTree/testData",
"compiler/testData/psi/kdoc",
"compiler/tests/org/jetbrains/kotlin/code/CodeConformanceTest.kt",
"compiler/util/src/org/jetbrains/kotlin/config/MavenComparableVersion.java",
"dependencies",
"dependencies/protobuf/protobuf-relocated/build",
"dist",
"idea/testData/codeInsight/renderingKDoc",
"js/js.tests/.gradle",
"js/js.tests/build",
"js/js.translator/qunit/qunit.js",
"js/js.translator/testData/node_modules",
"local",
"libraries/kotlin.test/js/it/.gradle",
"libraries/kotlin.test/js/it/node_modules",
"libraries/reflect/api/src/java9/java/kotlin/reflect/jvm/internal/impl",
"libraries/reflect/build",
"libraries/stdlib/jdk8/moduleTest/NonExportedPackagesTest.kt",
"libraries/stdlib/js-ir/.gradle",
"libraries/stdlib/js-ir/build",
"libraries/stdlib/js-ir-minimal-for-test/.gradle",
"libraries/stdlib/js-ir-minimal-for-test/build",
"libraries/stdlib/js-v1/.gradle",
"libraries/stdlib/js-v1/build",
"libraries/tools/binary-compatibility-validator/src/main/kotlin/org.jetbrains.kotlin.tools",
"libraries/tools/kotlin-gradle-plugin-core/gradle_api_jar/build/tmp",
"libraries/tools/kotlin-gradle-plugin-integration-tests/out",
"libraries/tools/kotlin-js-tests/src/test/web/qunit.js",
"libraries/tools/kotlin-maven-plugin/target",
"libraries/tools/kotlin-source-map-loader/.gradle",
"libraries/tools/kotlin-test-js-runner/.gradle",
"libraries/tools/kotlin-test-js-runner/lib",
"libraries/tools/kotlin-test-js-runner/node_modules",
"libraries/tools/kotlin-test-nodejs-runner/.gradle",
"libraries/tools/kotlin-test-nodejs-runner/node_modules",
"libraries/tools/kotlinp/src",
"libraries/tools/new-project-wizard/new-project-wizard-cli/build",
"out",
"kotlin-native/build",
"kotlin-native/performance",
"kotlin-native/samples"
)
)
private val COPYRIGHT_EXCLUDED_FILES_AND_DIRS_MATCHER = FileMatcher(
File("."),
listOf(
"build",
"buildSrc/build/generated-sources",
"buildSrc/prepare-deps/build",
"compiler/ir/serialization.js/build/fullRuntime",
"compiler/ir/serialization.js/build/reducedRuntime/src/libraries/stdlib/js-ir/runtime/longjs.kt",
"compiler/tests/org/jetbrains/kotlin/code/CodeConformanceTest.kt",
"dependencies",
"dependencies/android-sdk/build",
"dependencies/protobuf/protobuf-relocated/build",
"dist",
"idea/idea-jvm/src/org/jetbrains/kotlin/idea/copyright",
"js/js.tests/.gradle",
"js/js.tests/build",
"js/js.translator/testData/node_modules",
"libraries/kotlin.test/js/it/.gradle",
"libraries/kotlin.test/js/it/node_modules",
"libraries/stdlib/common/build",
"libraries/stdlib/js-ir/.gradle",
"libraries/stdlib/js-ir/build",
"libraries/stdlib/js-ir/build/",
"libraries/stdlib/js-ir/runtime/longjs.kt",
"libraries/stdlib/js-ir-minimal-for-test/.gradle",
"libraries/stdlib/js-ir-minimal-for-test/build",
"libraries/stdlib/js-v1/.gradle",
"libraries/stdlib/js-v1/build",
"libraries/stdlib/js-v1/node_modules",
"libraries/stdlib/wasm/build",
"libraries/tools/kotlin-gradle-plugin-integration-tests/build",
"libraries/tools/kotlin-gradle-plugin-integration-tests/.testKitDir",
"libraries/tools/kotlin-gradle-plugin-integration-tests/out",
"libraries/tools/kotlin-maven-plugin-test/target",
"libraries/tools/kotlin-test-js-runner/.gradle",
"libraries/tools/kotlin-test-js-runner/lib",
"libraries/tools/kotlin-test-js-runner/node_modules",
"libraries/tools/kotlin-test-nodejs-runner/.gradle",
"libraries/tools/kotlin-test-nodejs-runner/node_modules",
"libraries/tools/kotlin-source-map-loader/.gradle",
"kotlin-native", "libraries/stdlib/native-wasm", // Have a separate licences manager
"out",
)
)
}
fun testParserCode() {
val pattern = Pattern.compile("assert.*?\\b[^_]at.*?$", Pattern.MULTILINE)
for (sourceFile in FileUtil.findFilesByMask(JAVA_FILE_PATTERN, File("compiler/frontend/src/org/jetbrains/kotlin/parsing"))) {
val matcher = pattern.matcher(sourceFile.readText())
if (matcher.find()) {
fail("An at-method with side-effects is used inside assert: ${matcher.group()}\nin file: $sourceFile")
}
}
}
private fun isCorrectExtension(filename: String, extensions: Set<String>): Boolean {
val additionalExtensions = listOf(
"after", "new", "before", "expected",
"todo", "delete", "touch", "prefix", "postfix", "map",
"fragment", "after2", "result", "log", "messages", "conflicts", "match", "imports", "txt", "xml"
)
val possibleAdditionalExtensions = extensions.plus(additionalExtensions)
val fileExtensions = filename.split("\\.").drop(1)
if (fileExtensions.size < 2) {
return true
}
val extension = fileExtensions.last()
return !((extension !in possibleAdditionalExtensions && (extension.toIntOrNull() ?: MAX_STEPS_COUNT) >= MAX_STEPS_COUNT))
}
fun testForgottenBunchDirectivesAndFiles() {
val sourceBunchFilePattern = Pattern.compile("(.+\\.java|.+\\.kt|.+\\.js)(\\.\\w+)?")
val root = nonSourcesMatcher.root
val extensions = File(root, ".bunch").readLines().map { it.split("_") }.flatten().toSet()
val failBuilder = mutableListOf<String>()
nonSourcesMatcher.excludeWalkTopDown(sourceBunchFilePattern).forEach { sourceFile ->
val matches = Regex("BUNCH (\\w+)")
.findAll(sourceFile.readText())
.map { it.groupValues[1] }
.toSet()
.filterNot { it in extensions }
for (bunch in matches) {
val filename = FileUtil.toSystemIndependentName(sourceFile.toRelativeString(root))
failBuilder.add("$filename has unregistered $bunch bunch directive")
}
if (!isCorrectExtension(sourceFile.name, extensions)) {
val filename = FileUtil.toSystemIndependentName(sourceFile.toRelativeString(root))
failBuilder.add("$filename has unknown bunch extension")
}
}
if (failBuilder.isNotEmpty()) {
fail("\n" + failBuilder.joinToString("\n"))
}
}
fun testNoBadSubstringsInProjectCode() {
class TestData(val message: String, val filter: (File, String) -> Boolean) {
val result: MutableList<File> = ArrayList()
}
val atAuthorPattern = Pattern.compile("/\\*.+@author.+\\*/", Pattern.DOTALL)
val tests = listOf(
TestData(
"%d source files contain @author javadoc tag.\nPlease remove them or exclude in this test:\n%s"
) { _, source ->
// substring check is an optimization
"@author" in source && atAuthorPattern.matcher(source).find() &&
"ASM: a very small and fast Java bytecode manipulation framework" !in source &&
"package org.jetbrains.kotlin.tools.projectWizard.settings.version.maven" !in source
},
TestData(
"%d source files use something from com.beust.jcommander.internal package.\n" +
"This code won't work when there's no TestNG in the classpath of our IDEA plugin, " +
"because there's only an optional dependency on testng.jar.\n" +
"Most probably you meant to use Guava's Lists, Maps or Sets instead. " +
"Please change references in these files to com.google.common.collect:\n%s"
) { _, source ->
"com.beust.jcommander.internal" in source
},
TestData(
"%d source files contain references to package org.jetbrains.jet.\n" +
"Package org.jetbrains.jet is deprecated now in favor of org.jetbrains.kotlin. " +
"Please consider changing the package in these files:\n%s"
) { _, source ->
"org.jetbrains.jet" in source
},
TestData(
"%d source files contain references to package kotlin.reflect.jvm.internal.impl.\n" +
"This package contains internal reflection implementation and is a result of a " +
"post-processing of kotlin-reflect.jar by jarjar.\n" +
"Most probably you meant to use classes from org.jetbrains.kotlin.**.\n" +
"Please change references in these files or exclude them in this test:\n%s"
) { _, source ->
"kotlin.reflect.jvm.internal.impl" in source
},
TestData(
"%d source files contain references to package org.objectweb.asm.\n" +
"Package org.jetbrains.org.objectweb.asm should be used instead to avoid troubles with different asm versions in classpath. " +
"Please consider changing the package in these files:\n%s"
) { _, source ->
" org.objectweb.asm" in source
}
)
nonSourcesMatcher.excludeWalkTopDown(SOURCES_FILE_PATTERN).forEach { sourceFile ->
val source = sourceFile.readText()
for (test in tests) {
if (test.filter(sourceFile, source)) test.result.add(sourceFile)
}
}
if (tests.flatMap { it.result }.isNotEmpty()) {
fail(buildString {
for (test in tests) {
if (test.result.isNotEmpty()) {
append(test.message.format(test.result.size, test.result.joinToString("\n")))
appendLine()
appendLine()
}
}
})
}
}
fun testThirdPartyCopyrights() {
val filesWithUnlistedCopyrights = mutableListOf<String>()
val knownThirdPartyCode = loadKnownThirdPartyCodeList()
val copyrightRegex = Regex("""\bCopyright\b""")
val root = COPYRIGHT_EXCLUDED_FILES_AND_DIRS_MATCHER.root
COPYRIGHT_EXCLUDED_FILES_AND_DIRS_MATCHER.excludeWalkTopDown(SOURCES_FILE_PATTERN)
.filter { sourceFile ->
val relativePath = FileUtil.toSystemIndependentName(sourceFile.toRelativeString(root))
!knownThirdPartyCode.any { relativePath.startsWith(it) }
}
.forEach { sourceFile ->
sourceFile.useLines { lineSequence ->
for (line in lineSequence) {
if (copyrightRegex in line && "JetBrains" !in line) {
val relativePath = FileUtil.toSystemIndependentName(sourceFile.toRelativeString(root))
filesWithUnlistedCopyrights.add("$relativePath: $line")
}
}
}
}
if (filesWithUnlistedCopyrights.isNotEmpty()) {
fail(
"The following files contain third-party copyrights and no license information. " +
"Please update license/README.md accordingly:\n${filesWithUnlistedCopyrights.joinToString("\n")}"
)
}
}
private class FileMatcher(val root: File, paths: Collection<String>) {
private val files = paths.map { File(it) }
private val paths = files.mapTo(HashSet()) { it.invariantSeparatorsPath }
private val relativePaths = files.filterTo(ArrayList()) { it.isDirectory }.mapTo(HashSet()) { it.invariantSeparatorsPath + "/" }
fun matchExact(file: File): Boolean {
return file.relativeTo(root).invariantSeparatorsPath in paths
}
fun matchWithContains(file: File): Boolean {
if (matchExact(file)) return true
val relativePath = file.relativeTo(root).invariantSeparatorsPath
return relativePaths.any { relativePath.startsWith(it) }
}
}
private fun FileMatcher.excludeWalkTopDown(filePattern: Pattern): Sequence<File> {
return root.walkTopDown()
.onEnter { dir ->
!matchExact(dir) // Don't enter to ignored dirs
}
.filter { file -> !matchExact(file) } // filter ignored files
.filter { file -> filePattern.matcher(file.name).matches() }
.filter { file -> file.isFile }
}
fun testRepositoriesAbuse() {
class RepoAllowList(val repo: String, root: File, allowList: Set<String>, val exclude: String? = null) {
val matcher = FileMatcher(root, allowList)
}
val root = nonSourcesMatcher.root
val repoCheckers = listOf(
RepoAllowList(
// Please use cache-redirector for importing in tests
"https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev", root,
setOf("repo/scripts/cache-redirector.settings.gradle.kts")
),
RepoAllowList(
"https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev", root,
setOf("repo/scripts/cache-redirector.settings.gradle.kts")
),
RepoAllowList(
// Please use cache-redirector for importing in tests
"https://maven.pkg.jetbrains.space/kotlin/p/kotlin/eap", root,
setOf("repo/scripts/cache-redirector.settings.gradle.kts")
),
RepoAllowList(
"https://cache-redirector.jetbrains.com/maven.pkg.jetbrains.space/kotlin/p/kotlin/dev", root,
setOf("repo/scripts/cache-redirector.settings.gradle.kts")
),
RepoAllowList(
"kotlin/ktor", root,
setOf("repo/scripts/cache-redirector.settings.gradle.kts")
),
RepoAllowList(
"bintray.com", root,
setOf("repo/scripts/cache-redirector.settings.gradle.kts"),
exclude = "jcenter.bintray.com"
)
)
data class RepoOccurance(val repo: String, val file: File)
data class RepoOccurrences(val repo: String, val files: Collection<File>)
val extensionsPattern = Pattern.compile(".+\\.(java|kt|gradle|kts|xml)(\\.\\w+)?")
val repoOccurrences: List<RepoOccurrences> = nonSourcesMatcher.excludeWalkTopDown(extensionsPattern)
.flatMap { file ->
val checkers = repoCheckers.filter { checker ->
!checker.matcher.matchWithContains(file)
}
if (checkers.isNotEmpty()) {
val occurrences = ArrayList<RepoOccurance>()
file.useLines { lines ->
for (line in lines) {
for (checker in checkers) {
if (line.contains(checker.repo) && (checker.exclude == null || !line.contains(checker.exclude))) {
occurrences.add(RepoOccurance(checker.repo, file))
}
}
}
}
occurrences
} else {
listOf()
}
}
.groupBy { it.repo }
.map { (repo, occurrences) -> RepoOccurrences(repo, occurrences.mapTo(HashSet()) { it.file }) }
if (repoOccurrences.isNotEmpty()) {
val repoOccurrencesStableOrder = repoOccurrences
.map { occurrence -> RepoOccurrences(occurrence.repo, occurrence.files.sortedBy { file -> file.path }) }
.sortedBy { it.repo }
fail(
buildString {
appendLine("The following files use repositories and not listed in the correspondent allow lists")
for ((repo, files) in repoOccurrencesStableOrder) {
appendLine(repo)
for (file in files) {
appendLine(" ${file.relativeTo(root).invariantSeparatorsPath}")
}
}
}
)
}
}
private fun loadKnownThirdPartyCodeList(): List<String> {
File("license/README.md").useLines { lineSequence ->
return lineSequence
.filter { it.startsWith(" - Path: ") }
.map { it.removePrefix(" - Path: ").trim().ensureFileOrEndsWithSlash() }
.toList()
}
}
fun testLanguageFeatureOrder() {
val values = enumValues<LanguageFeature>()
val enabledFeatures = values.filter { it.sinceVersion != null }
if (enabledFeatures.sortedBy { it.sinceVersion!! } != enabledFeatures) {
val (a, b) = enabledFeatures.zipWithNext().first { (a, b) -> a.sinceVersion!! > b.sinceVersion!! }
fail(
"Please make sure LanguageFeature entries are sorted by sinceVersion to improve readability & reduce confusion.\n" +
"The feature $b is out of order; its sinceVersion is ${b.sinceVersion}, yet it comes after $a, whose " +
"sinceVersion is ${a.sinceVersion}.\n"
)
}
}
}
private fun String.ensureFileOrEndsWithSlash() =
if (endsWith("/") || "." in substringAfterLast('/')) this else "$this/"
@@ -0,0 +1,70 @@
/*
* Copyright 2010-2022 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.code
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase
import org.junit.Test
import java.io.File
class DepsVerificationMetadataTest {
@JacksonXmlRootElement(localName = "verification-metadata")
private data class VerificationMetadata(
@field:JacksonXmlElementWrapper(localName = "components")
val components: List<Component> = listOf()
)
private data class Component(
@field:JacksonXmlProperty(isAttribute = true)
val group: String,
@field:JacksonXmlProperty(isAttribute = true)
val name: String,
@field:JacksonXmlProperty(isAttribute = true)
val version: String,
@field:JacksonXmlElementWrapper(useWrapping = false)
val artifact: List<Artifact>,
)
private data class Artifact(
@field:JacksonXmlProperty(isAttribute = true)
val name: String,
val md5: Hash?,
val sha256: Hash?
)
private data class Hash(@field:JacksonXmlProperty(isAttribute = true) val value: String)
@Test
fun dependenciesHasValidHashes() {
val mapper = XmlMapper()
.apply {
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
registerKotlinModule()
}
val verificationMetadata = mapper.readValue<VerificationMetadata>(File("gradle/verification-metadata.xml"))
val fails = mutableListOf<String>()
verificationMetadata.components.forEach { component ->
component.artifact.forEach { artifact ->
if (artifact.md5 == null) {
fails.add("${artifact.name} - md5 is missing")
}
if (artifact.sha256 == null) {
fails.add("${artifact.name} - sha256 is missing")
}
}
}
KtUsefulTestCase.assertEmpty(fails)
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2010-2022 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.code
import junit.framework.TestCase
import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime
import org.jetbrains.kotlin.load.java.JvmAbi
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import kotlin.reflect.jvm.javaField
class ReflectionCodeSanityTest : TestCase() {
private lateinit var classLoader: ClassLoader
override fun setUp() {
super.setUp()
classLoader = ForTestCompileRuntime.runtimeAndReflectJarClassLoader()
}
override fun tearDown() {
ReflectionCodeSanityTest::classLoader.javaField!!.set(this, null)
super.tearDown()
}
private fun loadClass(name: String): Class<*> =
classLoader.loadClass("kotlin.reflect.jvm.internal.$name")
private fun collectClassesWithSupers(vararg names: String): Set<Class<*>> {
val result = linkedSetOf<Class<*>>()
fun addClassToCheck(klass: Class<*>) {
if (result.add(klass)) {
klass.superclass?.let(::addClassToCheck)
}
}
for (name in names) {
addClassToCheck(loadClass(name))
}
return result
}
fun testNoDelegatedPropertiesInKClassAndKProperties() {
val badFields = linkedSetOf<Field>()
for (klass in collectClassesWithSupers(
"KClassImpl",
"KMutableProperty0Impl",
"KMutableProperty1Impl",
"KMutableProperty2Impl"
)) {
badFields.addAll(klass.declaredFields.filter { it.name.endsWith(JvmAbi.DELEGATED_PROPERTY_NAME_SUFFIX) })
}
if (badFields.isNotEmpty()) {
fail("The fields listed below appear to be delegates for properties.\n" +
"It's highly not recommended to use property delegates in reflection.jvm because a KProperty instance\n" +
"is created for each delegated property and that makes the initialization sequence of reflection\n" +
"implementation classes unpredictable and leads to a deadlock or ExceptionInInitializerError.\n\n" +
"Please un-delegate the corresponding properties:\n\n" +
badFields.joinToString("\n"))
}
}
fun testMaxAllowedFields() {
// The following classes are instantiated a lot in Kotlin applications and thus they should be optimized as best as possible.
// This test checks that these classes have not more fields than a predefined small number, which can usually be calculated as
// the number of constructor parameters (number of objects needed to initialize an instance) + 1 for 'data', the reflection cache.
val classesWithMaxAllowedFields = linkedMapOf(
"KClassImpl" to 2, // jClass, data
"KPackageImpl" to 3 // jClass, moduleName, data
)
val badClasses = linkedMapOf<Class<*>, Collection<Field>>()
for ((className, maxAllowedFields) in classesWithMaxAllowedFields) {
val klass = loadClass(className)
val fields = generateSequence(klass) { it.superclass }
.flatMap { it.declaredFields.asSequence() }
.filterNot { Modifier.isStatic(it.modifiers) }
.toList()
if (fields.size > maxAllowedFields) {
badClasses[klass] = fields
}
}
if (badClasses.isNotEmpty()) {
fail("Some classes in reflection.jvm contain more fields than it is allowed. Please optimize storage in these classes:\n\n" +
badClasses.entries.joinToString("\n") { entry ->
val (klass, fields) = entry
"$klass has ${fields.size} fields but max allowed = ${classesWithMaxAllowedFields[klass.simpleName]}:\n" +
fields.joinToString("\n") { " $it" }
})
}
}
}