[LL FIR] KT-50732 Add support for LL FIR-specific tests (.ll.kt)

- `.ll.kt` test data can be added in cases where LL FIR resolution
  legally diverges from K2 compiler results.
- Each `.ll.kt` test is prefixed with an `LL_FIR_DIVERGENCE` directive
  which must explain why the test may diverge from K2 compiler results.
  - `LLFirDivergenceCommentChecker` ensures that each `.ll.kt` file
    contains an `LL_FIR_DIVERGENCE` directive.
- `LLFirIdenticalChecker` results in an assertion error if the `.ll.kt`
  test and its base test are completely identical, including in their
  meta info (but ignoring `LL_FIR_DIVERGENCE`).
  - The checker additionally ensures that the base source file and the
    `.ll.kt` source file have identical Kotlin source code (ignoring
    meta info and `LL_FIR_DIVERGENCE`). This ensures that both tests
    test the exact same thing.
- `.ll.kt` files are ignored by select test generators, in addition to
  `.fir.kt` files.
This commit is contained in:
Marco Pennekamp
2022-12-02 19:01:54 +01:00
committed by teamcity
parent b0db0f59b4
commit 88ac5727cc
52 changed files with 3706 additions and 3010 deletions
@@ -50,6 +50,10 @@ abstract class AbstractCompilerBasedTestForFir : AbstractCompilerBasedTest() {
firHandlersStep {
useHandlers(::LLDiagnosticParameterChecker)
}
useMetaTestConfigurators(::LLFirMetaTestConfigurator)
useAfterAnalysisCheckers(::LLFirIdenticalChecker)
useAfterAnalysisCheckers(::LLFirDivergenceCommentChecker)
}
open fun TestConfigurationBuilder.configureTest() {}
@@ -0,0 +1,33 @@
/*
* 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.analysis.low.level.api.fir.compiler.based
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.assertions
import org.jetbrains.kotlin.test.services.moduleStructure
import org.jetbrains.kotlin.test.utils.isLLFirTestData
class LLFirDivergenceCommentChecker(testServices: TestServices) : AfterAnalysisChecker(testServices) {
override fun check(failedAssertions: List<WrappedException>) {
val testDataFile = testServices.moduleStructure.originalTestDataFiles.first()
if (!testDataFile.isLLFirTestData) return
if (!testDataFile.hasLlFirDivergenceDirective()) {
testServices.assertions.fail {
"""The LL FIR test data file `${testDataFile.name}` is missing an `LL_FIR_DIVERGENCE` directive. At the beginning of the
|file, add the following directive:
|
|// LL_FIR_DIVERGENCE
|// A comment describing why the LL FIR result is diverging from the compiler result. You must provide a good reason, or
|// otherwise the divergence is probably a bug in LL FIR which needs to be fixed. Try to be as specific as possible.
|// LL_FIR_DIVERGENCE
|""".trimMargin()
}
}
}
}
@@ -0,0 +1,68 @@
/*
* 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.analysis.low.level.api.fir.compiler.based
import java.io.File
private const val LL_FIR_DIVERGENCE_DIRECTIVE = "LL_FIR_DIVERGENCE"
private const val LL_FIR_DIVERGENCE_DIRECTIVE_COMMENT = "// $LL_FIR_DIVERGENCE_DIRECTIVE"
/**
* Checks whether the [File] contains a legal `LL_FIR_DIVERGENCE` directive without reading the whole file.
*/
fun File.hasLlFirDivergenceDirective(): Boolean = useLines { findDirectiveInLines(it.iterator()) }
fun String.removeLlFirDivergenceDirective(trimLines: Boolean): String {
// To ignore `LL_FIR_DIVERGENCE`, we advance `iterator` with `findDirectiveInLines` and then concatenate the rest of the lines.
val iterator = this.lineSequence().iterator()
return if (findDirectiveInLines(iterator)) {
// `trimStart` ensures that the `LL_FIR_DIVERGENCE` directive can be separated from the rest of the file by blank lines.
iterator.asSequence().concatLines(trimLines).trimStart()
} else this
}
private fun Sequence<String>.concatLines(trimLines: Boolean): String =
if (trimLines) joinToString("\n") { it.trimEnd() }.trimEnd()
else joinToString("\n")
/**
* Tries to find the `LL_FIR_DIVERGENCE` directive in the lines given by [iterator] and returns whether this is the case. If the directive
* was found, [iterator] is guaranteed to be advanced exactly past the `LL_FIR_DIVERGENCE` directive.
*
* The format of the directive is as such:
*
* ```
* // LL_FIR_DIVERGENCE
* // lorem ipsum
* // dolor sit amet
* // LL_FIR_DIVERGENCE
* ```
*
* Blank lines before the directive or inside the directive region are ignored.
*/
private fun findDirectiveInLines(iterator: Iterator<String>): Boolean {
val firstNonBlankLine = iterator.nextNonBlankLineTrimmed()
if (firstNonBlankLine != LL_FIR_DIVERGENCE_DIRECTIVE_COMMENT) return false
// Ignore line comments and blank lines until the second (closing) `LL_FIR_DIVERGENCE` is found. Any other text, such as uncommented
// code inside the directive region, is illegal.
while (iterator.hasNext()) {
val line = iterator.nextNonBlankLineTrimmed() ?: return false
if (line.startsWith("//")) {
if (line == LL_FIR_DIVERGENCE_DIRECTIVE_COMMENT) return true
} else return false
}
return false
}
private fun Iterator<String>.nextNonBlankLineTrimmed(): String? {
while (hasNext()) {
val line = next().trimEnd()
if (line.isNotEmpty()) return line
}
return null
}
@@ -0,0 +1,85 @@
/*
* 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.analysis.low.level.api.fir.compiler.based
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.frontend.fir.handlers.AbstractFirIdenticalChecker
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.assertions
import org.jetbrains.kotlin.test.services.sourceFileProvider
import org.jetbrains.kotlin.test.utils.isLLFirTestData
import java.io.File
import kotlin.test.assertEquals
/**
* `.ll.kt` test data should not be identical to its base `.fir.kt`/`.kt` test data. If a base `.fir.kt` file does not exist, the base file
* is the `.kt` file.
*
* As the `LL_FIR_DIVERGENCE` directive only exists in `.ll.kt` files, [LLFirIdenticalChecker] ignores this directive when comparing the
* LL FIR file's content to the base file's content.
*/
class LLFirIdenticalChecker(testServices: TestServices) : AbstractFirIdenticalChecker(testServices) {
override fun checkTestDataFile(testDataFile: File) {
if (!testDataFile.isLLFirTestData) return
val originalFile = helper.getClassicFileToCompare(testDataFile)
val baseFile = helper.getFirFileToCompare(originalFile).takeIf { it.exists() } ?: originalFile
// `readContentIgnoringLlFirDivergenceDirective` trims whitespace after the `LL_FIR_DIVERGENCE` directive to allow blank lines
// after the directive. Hence, the base content's starting whitespace needs to be trimmed as well, otherwise file contents might
// differ in their starting whitespace.
val baseContent = helper.readContent(baseFile, trimLines = true).trimStart()
val llContent = helper.readContent(testDataFile, trimLines = false).removeLlFirDivergenceDirective(trimLines = true)
if (baseContent == llContent) {
testServices.assertions.fail {
"`${testDataFile.name}` and `${baseFile.name}` are identical. Remove `$testDataFile`."
}
} else {
assertPreprocessedTestDataAreEqual(baseFile, baseContent, testDataFile, llContent) {
"When ignoring diagnostics, the contents of `${baseFile.name}` (expected) and `${testDataFile.name}` (actual) are not" +
" identical. `.ll.kt` test data may only differ from its base `.fir.kt` or `.kt` test data in the reported" +
" diagnostics and the `LL_FIR_DIVERGENCE` directive. Update one of these test data files."
}
}
}
/**
* Asserts that [baseFile] and [llFile] have the same content after preprocessing (which removes diagnostics and other meta info). This
* prevents situations where one test data changes, but changes to the other test data are forgotten.
*
* [llContent] should have its `LL_FIR_DIVERGENCE` directive already removed.
*/
private fun assertPreprocessedTestDataAreEqual(
baseFile: File,
baseContent: String,
llFile: File,
llContent: String,
message: () -> String,
) {
val processedBaseContent = testServices.sourceFileProvider.getContentOfSourceFile(
TestFile(
baseFile.path,
baseContent,
baseFile,
startLineNumberInOriginalFile = 0,
isAdditional = false,
RegisteredDirectives.Empty,
)
)
val processedLlContent = testServices.sourceFileProvider.getContentOfSourceFile(
TestFile(
llFile.path,
llContent,
llFile,
startLineNumberInOriginalFile = 0,
isAdditional = false,
RegisteredDirectives.Empty,
)
)
assertEquals(processedBaseContent, processedLlContent, message())
}
}
@@ -0,0 +1,21 @@
/*
* 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.analysis.low.level.api.fir.compiler.based
import org.jetbrains.kotlin.test.services.MetaTestConfigurator
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.utils.llFirTestDataFile
import java.io.File
/**
* Uses `.ll.kt` test data if available.
*/
class LLFirMetaTestConfigurator(testServices: TestServices) : MetaTestConfigurator(testServices) {
override fun transformTestDataPath(testDataFileName: String): String {
val llFirFile = File(testDataFileName).llFirTestDataFile
return if (llFirFile.exists()) llFirFile.path else testDataFileName
}
}