[FIR-TEST] Move abstract diagnostics tests to tests-common module
This commit is contained in:
+318
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2010-2019 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.fir
|
||||
|
||||
import com.intellij.openapi.extensions.Extensions
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiElementFinder
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import org.jetbrains.kotlin.analyzer.ModuleInfo
|
||||
import org.jetbrains.kotlin.asJava.finder.JavaElementFinder
|
||||
import org.jetbrains.kotlin.checkers.BaseDiagnosticsTest
|
||||
import org.jetbrains.kotlin.checkers.DiagnosticDiffCallbacks
|
||||
import org.jetbrains.kotlin.checkers.diagnostics.ActualDiagnostic
|
||||
import org.jetbrains.kotlin.checkers.diagnostics.PositionalTextDiagnostic
|
||||
import org.jetbrains.kotlin.checkers.diagnostics.SyntaxErrorDiagnostic
|
||||
import org.jetbrains.kotlin.checkers.diagnostics.TextDiagnostic
|
||||
import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
|
||||
import org.jetbrains.kotlin.diagnostics.FirErrors
|
||||
import org.jetbrains.kotlin.diagnostics.PsiDiagnosticUtils
|
||||
import org.jetbrains.kotlin.fir.builder.RawFirBuilder
|
||||
import org.jetbrains.kotlin.fir.declarations.FirFile
|
||||
import org.jetbrains.kotlin.fir.java.FirJavaModuleBasedSession
|
||||
import org.jetbrains.kotlin.fir.java.FirLibrarySession
|
||||
import org.jetbrains.kotlin.fir.java.FirProjectSessionProvider
|
||||
import org.jetbrains.kotlin.fir.lightTree.LightTree2Fir
|
||||
import org.jetbrains.kotlin.fir.resolve.diagnostics.ConeDiagnostic
|
||||
import org.jetbrains.kotlin.fir.resolve.firProvider
|
||||
import org.jetbrains.kotlin.fir.resolve.impl.FirProviderImpl
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.resolve.AnalyzingUtils
|
||||
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
|
||||
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatformAnalyzerServices
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
abstract class AbstractFirBaseDiagnosticsTest : BaseDiagnosticsTest() {
|
||||
override fun analyzeAndCheck(testDataFile: File, files: List<TestFile>) {
|
||||
try {
|
||||
analyzeAndCheckUnhandled(testDataFile, files)
|
||||
} catch (t: AssertionError) {
|
||||
throw t
|
||||
} catch (t: Throwable) {
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
override fun createEnvironment(file: File): KotlinCoreEnvironment {
|
||||
return super.createEnvironment(file).apply {
|
||||
Extensions.getArea(this.project)
|
||||
.getExtensionPoint(PsiElementFinder.EP_NAME)
|
||||
.unregisterExtension(JavaElementFinder::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
open fun analyzeAndCheckUnhandled(testDataFile: File, files: List<TestFile>, useLightTree: Boolean = false) {
|
||||
val groupedByModule = files.groupBy(TestFile::module)
|
||||
|
||||
val modules = createModules(groupedByModule)
|
||||
|
||||
val sessionProvider = FirProjectSessionProvider(project)
|
||||
|
||||
//For BuiltIns, registered in sessionProvider automatically
|
||||
val allProjectScope = GlobalSearchScope.allScope(project)
|
||||
FirLibrarySession.create(
|
||||
builtInsModuleInfo, sessionProvider, allProjectScope, project,
|
||||
environment.createPackagePartProvider(allProjectScope)
|
||||
)
|
||||
|
||||
val configToSession = modules.mapValues { (config, info) ->
|
||||
val moduleFiles = groupedByModule.getValue(config)
|
||||
val scope = TopDownAnalyzerFacadeForJVM.newModuleSearchScope(
|
||||
project,
|
||||
moduleFiles.mapNotNull { it.ktFile })
|
||||
FirJavaModuleBasedSession(info, sessionProvider, scope)
|
||||
}
|
||||
|
||||
val firFilesPerSession = mutableMapOf<FirSession, List<FirFile>>()
|
||||
|
||||
// TODO: make module/session/transformer handling like in AbstractFirMultiModuleTest (IDE)
|
||||
for ((testModule, testFilesInModule) in groupedByModule) {
|
||||
val ktFiles = getKtFiles(testFilesInModule, true)
|
||||
|
||||
val session = configToSession.getValue(testModule)
|
||||
|
||||
val firFiles = mutableListOf<FirFile>()
|
||||
mapKtFilesToFirFiles(session, ktFiles, firFiles, useLightTree)
|
||||
firFilesPerSession[session] = firFiles
|
||||
}
|
||||
|
||||
runAnalysis(testDataFile, files, firFilesPerSession)
|
||||
}
|
||||
|
||||
private fun mapKtFilesToFirFiles(session: FirSession, ktFiles: List<KtFile>, firFiles: MutableList<FirFile>, useLightTree: Boolean) {
|
||||
val firProvider = (session.firProvider as FirProviderImpl)
|
||||
if (useLightTree) {
|
||||
val lightTreeBuilder = LightTree2Fir(session, firProvider.kotlinScopeProvider, stubMode = false)
|
||||
ktFiles.mapTo(firFiles) {
|
||||
val firFile = lightTreeBuilder.buildFirFile(it.text, it.name)
|
||||
(session.firProvider as FirProviderImpl).recordFile(firFile)
|
||||
firFile
|
||||
}
|
||||
} else {
|
||||
val firBuilder = RawFirBuilder(session, firProvider.kotlinScopeProvider, false)
|
||||
ktFiles.mapTo(firFiles) {
|
||||
val firFile = firBuilder.buildFirFile(it)
|
||||
firProvider.recordFile(firFile)
|
||||
firFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun runAnalysis(testDataFile: File, testFiles: List<TestFile>, firFilesPerSession: Map<FirSession, List<FirFile>>)
|
||||
|
||||
private fun createModules(
|
||||
groupedByModule: Map<TestModule?, List<TestFile>>
|
||||
): MutableMap<TestModule?, ModuleInfo> {
|
||||
val modules =
|
||||
HashMap<TestModule?, ModuleInfo>()
|
||||
|
||||
for (testModule in groupedByModule.keys) {
|
||||
val module = if (testModule == null)
|
||||
createSealedModule()
|
||||
else
|
||||
createModule(testModule.name)
|
||||
|
||||
modules[testModule] = module
|
||||
}
|
||||
|
||||
for (testModule in groupedByModule.keys) {
|
||||
if (testModule == null) continue
|
||||
|
||||
val module = modules[testModule]!!
|
||||
val dependencies = ArrayList<ModuleInfo>()
|
||||
dependencies.add(module)
|
||||
for (dependency in testModule.getDependencies()) {
|
||||
dependencies.add(modules[dependency]!!)
|
||||
}
|
||||
|
||||
|
||||
dependencies.add(builtInsModuleInfo)
|
||||
//dependencies.addAll(getAdditionalDependencies(module))
|
||||
(module as TestModuleInfo).dependencies.addAll(dependencies)
|
||||
}
|
||||
|
||||
return modules
|
||||
}
|
||||
|
||||
private val builtInsModuleInfo = BuiltInModuleInfo(Name.special("<built-ins>"))
|
||||
|
||||
protected open fun createModule(moduleName: String): TestModuleInfo {
|
||||
parseModulePlatformByName(moduleName)
|
||||
return TestModuleInfo(Name.special("<$moduleName>"))
|
||||
}
|
||||
|
||||
class BuiltInModuleInfo(override val name: Name) :
|
||||
ModuleInfo {
|
||||
override val platform: TargetPlatform
|
||||
get() = JvmPlatforms.unspecifiedJvmPlatform
|
||||
|
||||
override val analyzerServices: PlatformDependentAnalyzerServices
|
||||
get() = JvmPlatformAnalyzerServices
|
||||
|
||||
override fun dependencies(): List<ModuleInfo> {
|
||||
return listOf(this)
|
||||
}
|
||||
}
|
||||
|
||||
protected class TestModuleInfo(override val name: Name) :
|
||||
ModuleInfo {
|
||||
override val platform: TargetPlatform
|
||||
get() = JvmPlatforms.unspecifiedJvmPlatform
|
||||
|
||||
override val analyzerServices: PlatformDependentAnalyzerServices
|
||||
get() = JvmPlatformAnalyzerServices
|
||||
|
||||
val dependencies = mutableListOf<ModuleInfo>(this)
|
||||
override fun dependencies(): List<ModuleInfo> {
|
||||
return dependencies
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun createSealedModule(): TestModuleInfo =
|
||||
createModule("test-module-jvm").apply {
|
||||
dependencies += builtInsModuleInfo
|
||||
}
|
||||
|
||||
protected fun TestFile.getActualText(
|
||||
coneDiagnostics: Iterable<ConeDiagnostic>,
|
||||
actualText: StringBuilder
|
||||
): Boolean {
|
||||
val ktFile = this.ktFile
|
||||
if (ktFile == null) {
|
||||
// TODO: check java files too
|
||||
actualText.append(this.clearText)
|
||||
return true
|
||||
}
|
||||
|
||||
if (ktFile.name.endsWith("CoroutineUtil.kt") && ktFile.packageFqName == FqName("helpers")) return true
|
||||
|
||||
// TODO: report JVM signature diagnostics also for implementing modules
|
||||
|
||||
val ok = booleanArrayOf(true)
|
||||
val diagnostics = coneDiagnostics.toActualDiagnostic(ktFile)
|
||||
val filteredDiagnostics = diagnostics // TODO
|
||||
|
||||
actualDiagnostics.addAll(filteredDiagnostics)
|
||||
|
||||
val uncheckedDiagnostics = mutableListOf<PositionalTextDiagnostic>()
|
||||
|
||||
val diagnosticToExpectedDiagnostic =
|
||||
CheckerTestUtil.diagnosticsDiff(
|
||||
diagnosedRanges,
|
||||
filteredDiagnostics,
|
||||
object : DiagnosticDiffCallbacks {
|
||||
override fun missingDiagnostic(
|
||||
diagnostic: TextDiagnostic,
|
||||
expectedStart: Int,
|
||||
expectedEnd: Int
|
||||
) {
|
||||
val message =
|
||||
"Missing " + diagnostic.description + PsiDiagnosticUtils.atLocation(
|
||||
ktFile,
|
||||
TextRange(
|
||||
expectedStart,
|
||||
expectedEnd
|
||||
)
|
||||
)
|
||||
System.err.println(message)
|
||||
ok[0] = false
|
||||
}
|
||||
|
||||
override fun wrongParametersDiagnostic(
|
||||
expectedDiagnostic: TextDiagnostic,
|
||||
actualDiagnostic: TextDiagnostic,
|
||||
start: Int,
|
||||
end: Int
|
||||
) {
|
||||
val message = "Parameters of diagnostic not equal at position " +
|
||||
PsiDiagnosticUtils.atLocation(
|
||||
ktFile,
|
||||
TextRange(
|
||||
start,
|
||||
end
|
||||
)
|
||||
) +
|
||||
". Expected: ${expectedDiagnostic.asString()}, actual: $actualDiagnostic"
|
||||
System.err.println(message)
|
||||
ok[0] = false
|
||||
}
|
||||
|
||||
override fun unexpectedDiagnostic(
|
||||
diagnostic: TextDiagnostic,
|
||||
actualStart: Int,
|
||||
actualEnd: Int
|
||||
) {
|
||||
val message =
|
||||
"Unexpected ${diagnostic.description}${PsiDiagnosticUtils.atLocation(
|
||||
ktFile,
|
||||
TextRange(
|
||||
actualStart,
|
||||
actualEnd
|
||||
)
|
||||
)}"
|
||||
System.err.println(message)
|
||||
ok[0] = false
|
||||
}
|
||||
|
||||
fun updateUncheckedDiagnostics(
|
||||
diagnostic: TextDiagnostic,
|
||||
start: Int,
|
||||
end: Int
|
||||
) {
|
||||
uncheckedDiagnostics.add(
|
||||
PositionalTextDiagnostic(
|
||||
diagnostic,
|
||||
start,
|
||||
end
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
actualText.append(
|
||||
CheckerTestUtil.addDiagnosticMarkersToText(
|
||||
ktFile,
|
||||
filteredDiagnostics,
|
||||
diagnosticToExpectedDiagnostic,
|
||||
{ file -> file.text },
|
||||
uncheckedDiagnostics,
|
||||
false,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
stripExtras(actualText)
|
||||
|
||||
return ok[0]
|
||||
}
|
||||
|
||||
private fun Iterable<ConeDiagnostic>.toActualDiagnostic(root: PsiElement): List<ActualDiagnostic> {
|
||||
val result = mutableListOf<ActualDiagnostic>()
|
||||
filter { it.diagnostic.factory != FirErrors.SYNTAX_ERROR }.mapTo(result) { ActualDiagnostic(it.diagnostic, null, true) }
|
||||
for (errorElement in AnalyzingUtils.getSyntaxErrorRanges(root)) {
|
||||
result.add(ActualDiagnostic(SyntaxErrorDiagnostic(errorElement), null, true))
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2010-2019 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.fir
|
||||
|
||||
import junit.framework.TestCase
|
||||
import org.jetbrains.kotlin.fir.declarations.FirFile
|
||||
import org.jetbrains.kotlin.fir.references.FirControlFlowGraphReference
|
||||
import org.jetbrains.kotlin.fir.resolve.dfa.FirControlFlowGraphReferenceImpl
|
||||
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode
|
||||
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.ControlFlowGraph
|
||||
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.EdgeKind
|
||||
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.FirControlFlowGraphRenderVisitor
|
||||
import org.jetbrains.kotlin.fir.resolve.diagnostics.collectors.AbstractDiagnosticCollector
|
||||
import org.jetbrains.kotlin.fir.resolve.diagnostics.collectors.FirDiagnosticsCollector
|
||||
import org.jetbrains.kotlin.fir.resolve.transformers.FirTotalResolveTransformer
|
||||
import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase
|
||||
import java.io.File
|
||||
|
||||
/*
|
||||
* For comfort viewing dumps of control flow graph you can setup external tool in IDEA that opens .dot files
|
||||
*
|
||||
* Example of config for `xdot` viewer:
|
||||
*
|
||||
* File -> Settings -> External tools -> Add
|
||||
*
|
||||
* Name: XDot
|
||||
* Program: xdot
|
||||
* Arguments: $FileNameWithoutExtension$.dot
|
||||
* Working directory: $FileDir$
|
||||
* Disable "Open console for tool output"
|
||||
*
|
||||
* After that you can run action `XDot` in editor with source of test (or with cfg dump)
|
||||
* and it will opens xdot with dump for that test
|
||||
*/
|
||||
abstract class AbstractFirDiagnosticsTest : AbstractFirBaseDiagnosticsTest() {
|
||||
companion object {
|
||||
val DUMP_CFG_DIRECTIVE = "DUMP_CFG"
|
||||
|
||||
val TestFile.withDumpCfgDirective: Boolean
|
||||
get() = DUMP_CFG_DIRECTIVE in directives
|
||||
|
||||
val File.cfgDumpFile: File
|
||||
get() = File(absolutePath.replace(".kt", ".dot"))
|
||||
}
|
||||
|
||||
override fun runAnalysis(testDataFile: File, testFiles: List<TestFile>, firFilesPerSession: Map<FirSession, List<FirFile>>) {
|
||||
for ((_, firFiles) in firFilesPerSession) {
|
||||
doFirResolveTestBench(
|
||||
firFiles,
|
||||
FirTotalResolveTransformer().transformers,
|
||||
gc = false
|
||||
)
|
||||
}
|
||||
val allFirFiles = firFilesPerSession.values.flatten()
|
||||
checkDiagnostics(testDataFile, testFiles, allFirFiles)
|
||||
checkFir(testDataFile, allFirFiles)
|
||||
|
||||
if (testFiles.any { it.withDumpCfgDirective }) {
|
||||
checkCfg(testDataFile, allFirFiles)
|
||||
checkCfgEdgeConsistency(allFirFiles)
|
||||
} else {
|
||||
checkCfgDumpNotExists(testDataFile)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkFir(testDataFile: File, firFiles: List<FirFile>) {
|
||||
val firFileDump = StringBuilder().apply { firFiles.forEach { it.accept(FirRenderer(this), null) } }.toString()
|
||||
val expectedPath = testDataFile.absolutePath.replace(".kt", ".txt")
|
||||
KotlinTestUtils.assertEqualsToFile(
|
||||
File(expectedPath),
|
||||
firFileDump
|
||||
)
|
||||
}
|
||||
|
||||
protected fun checkDiagnostics(file: File, testFiles: List<TestFile>, firFiles: List<FirFile>) {
|
||||
val collector = createCollector()
|
||||
val actualText = StringBuilder()
|
||||
for (testFile in testFiles) {
|
||||
val firFile = firFiles.firstOrNull { it.psi == testFile.ktFile }
|
||||
if (firFile != null) {
|
||||
val coneDiagnostics = collector.collectDiagnostics(firFile)
|
||||
testFile.getActualText(coneDiagnostics, actualText)
|
||||
} else {
|
||||
actualText.append(testFile.expectedText)
|
||||
}
|
||||
}
|
||||
KotlinTestUtils.assertEqualsToFile(file, actualText.toString())
|
||||
}
|
||||
|
||||
protected fun createCollector(): AbstractDiagnosticCollector {
|
||||
return FirDiagnosticsCollector.create()
|
||||
}
|
||||
|
||||
private fun checkCfg(testDataFile: File, firFiles: List<FirFile>) {
|
||||
val builder = StringBuilder()
|
||||
|
||||
firFiles.first().accept(FirControlFlowGraphRenderVisitor(builder), null)
|
||||
|
||||
val dotCfgDump = builder.toString()
|
||||
KotlinTestUtils.assertEqualsToFile(testDataFile.cfgDumpFile, dotCfgDump)
|
||||
}
|
||||
|
||||
private fun checkCfgEdgeConsistency(firFiles: List<FirFile>) {
|
||||
firFiles.forEach { it.accept(CfgConsistencyChecker) }
|
||||
}
|
||||
|
||||
private object CfgConsistencyChecker : FirVisitorVoid() {
|
||||
override fun visitElement(element: FirElement) {
|
||||
element.acceptChildren(this)
|
||||
}
|
||||
|
||||
override fun visitControlFlowGraphReference(controlFlowGraphReference: FirControlFlowGraphReference) {
|
||||
val graph = (controlFlowGraphReference as? FirControlFlowGraphReferenceImpl)?.controlFlowGraph ?: return
|
||||
checkConsistency(graph)
|
||||
}
|
||||
|
||||
private fun checkConsistency(graph: ControlFlowGraph) {
|
||||
for (node in graph.nodes) {
|
||||
for (to in node.followingNodes) {
|
||||
checkEdge(node, to)
|
||||
}
|
||||
for (from in node.previousNodes) {
|
||||
checkEdge(from, node)
|
||||
}
|
||||
TestCase.assertTrue(node.followingNodes.isNotEmpty() || node.previousNodes.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkEdge(from: CFGNode<*>, to: CFGNode<*>) {
|
||||
KtUsefulTestCase.assertContainsElements(from.followingNodes, to)
|
||||
KtUsefulTestCase.assertContainsElements(to.previousNodes, from)
|
||||
val fromKind = from.outgoingEdges.getValue(to)
|
||||
val toKind = to.incomingEdges.getValue(from)
|
||||
TestCase.assertEquals(fromKind, toKind)
|
||||
if (from.isDead || to.isDead) {
|
||||
KtUsefulTestCase.assertContainsElements(listOf(EdgeKind.Dead, EdgeKind.Cfg), toKind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkCfgDumpNotExists(testDataFile: File) {
|
||||
val cfgDumpFile = testDataFile.cfgDumpFile
|
||||
if (cfgDumpFile.exists()) {
|
||||
val message = """
|
||||
Directive `!$DUMP_CFG_DIRECTIVE` is missing, but file with cfg dump is present.
|
||||
Please remove ${cfgDumpFile.path} or add `!$DUMP_CFG_DIRECTIVE` to test
|
||||
""".trimIndent()
|
||||
kotlin.test.fail(message)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2010-2019 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.fir
|
||||
|
||||
import org.jetbrains.kotlin.test.InTextDirectivesUtils
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import java.io.File
|
||||
|
||||
abstract class AbstractFirDiagnosticsWithLightTreeTest : AbstractFirDiagnosticsTest(), FirDiagnosticsTestLightTreeHelper {
|
||||
override fun doTest(filePath: String) {
|
||||
val file = createTestFileFromPath(filePath)
|
||||
val expectedText = KotlinTestUtils.doLoadFile(file)
|
||||
if (InTextDirectivesUtils.isDirectiveDefined(expectedText, "// IGNORE_LIGHT_TREE")) return
|
||||
|
||||
super.doTest(filePath)
|
||||
}
|
||||
|
||||
override fun analyzeAndCheck(testDataFile: File, files: List<TestFile>) {
|
||||
super<FirDiagnosticsTestLightTreeHelper>.analyzeAndCheck(testDataFile, files)
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2010-2019 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.fir
|
||||
|
||||
import org.jetbrains.kotlin.fir.declarations.FirFile
|
||||
import org.jetbrains.kotlin.fir.resolve.transformers.FirTotalResolveTransformer
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import java.io.File
|
||||
|
||||
abstract class AbstractFirOldFrontendDiagnosticsTest : AbstractFirDiagnosticsTest() {
|
||||
override fun createTestFileFromPath(filePath: String): File {
|
||||
val newPath = filePath.replace(".kt", ".fir.kt")
|
||||
return File(newPath).also {
|
||||
prepareTestDataFile(filePath, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareTestDataFile(originalFilePath: String, firTestDataFile: File) {
|
||||
if (!firTestDataFile.exists()) {
|
||||
KotlinTestUtils.assertEqualsToFile(firTestDataFile, loadTestDataWithoutDiagnostics(File(originalFilePath)))
|
||||
}
|
||||
}
|
||||
|
||||
override fun runAnalysis(testDataFile: File, testFiles: List<TestFile>, firFilesPerSession: Map<FirSession, List<FirFile>>) {
|
||||
if (testFiles.any { it.directives.containsKey("FIR_IGNORE") }) return
|
||||
val failure: AssertionError? = try {
|
||||
for ((_, firFiles) in firFilesPerSession) {
|
||||
doFirResolveTestBench(firFiles, FirTotalResolveTransformer().transformers, gc = false)
|
||||
}
|
||||
null
|
||||
} catch (e: AssertionError) {
|
||||
e
|
||||
}
|
||||
val failureFile = File(testDataFile.path.replace(".kt", ".fail"))
|
||||
if (failure == null) {
|
||||
val allFirFiles = firFilesPerSession.values.flatten()
|
||||
checkResultingFirFiles(allFirFiles, testDataFile)
|
||||
assertFalse("Test is good but there is expected exception", failureFile.exists())
|
||||
checkDiagnostics(testDataFile, testFiles, allFirFiles)
|
||||
val needDump = testFiles.any { it.directives.containsKey("FIR_DUMP") }
|
||||
if (needDump) {
|
||||
checkFir(testDataFile, allFirFiles)
|
||||
}
|
||||
} else {
|
||||
if (!failureFile.exists()) {
|
||||
throw failure
|
||||
}
|
||||
checkFailureFile(failure, failureFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkFailureFile(failure: AssertionError, failureFile: File) {
|
||||
val failureMessage = buildString {
|
||||
appendln(failure.message)
|
||||
failure.cause?.let {
|
||||
append("Cause: ")
|
||||
appendln(it)
|
||||
}
|
||||
}
|
||||
KotlinTestUtils.assertEqualsToFile(failureFile, failureMessage)
|
||||
}
|
||||
|
||||
protected open fun checkResultingFirFiles(firFiles: List<FirFile>, testDataFile: File) {}
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2010-2019 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.fir
|
||||
|
||||
import org.jetbrains.kotlin.test.ConfigurationKind
|
||||
|
||||
abstract class AbstractFirOldFrontendDiagnosticsTestWithStdlib : AbstractFirOldFrontendDiagnosticsTest() {
|
||||
override fun getConfigurationKind(): ConfigurationKind {
|
||||
return ConfigurationKind.NO_KOTLIN_REFLECT
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2010-2019 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.fir
|
||||
|
||||
import org.jetbrains.kotlin.checkers.BaseDiagnosticsTest
|
||||
import java.io.File
|
||||
|
||||
interface FirDiagnosticsTestLightTreeHelper {
|
||||
fun analyzeAndCheck(testDataFile: File, files: List<BaseDiagnosticsTest.TestFile>) {
|
||||
try {
|
||||
analyzeAndCheckUnhandled(testDataFile, files, useLightTree = true)
|
||||
} catch (t: AssertionError) {
|
||||
throw t
|
||||
} catch (t: Throwable) {
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
fun analyzeAndCheckUnhandled(testDataFile: File, files: List<BaseDiagnosticsTest.TestFile>, useLightTree: Boolean)
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
/*
|
||||
* Copyright 2010-2018 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.fir
|
||||
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.psi.PsiElement
|
||||
import org.jetbrains.kotlin.fir.builder.RawFirBuilder
|
||||
import org.jetbrains.kotlin.fir.declarations.FirFile
|
||||
import org.jetbrains.kotlin.fir.diagnostics.FirStubDiagnostic
|
||||
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
|
||||
import org.jetbrains.kotlin.fir.expressions.FirQualifiedAccessExpression
|
||||
import org.jetbrains.kotlin.fir.lightTree.LightTree2Fir
|
||||
import org.jetbrains.kotlin.fir.references.FirErrorNamedReference
|
||||
import org.jetbrains.kotlin.fir.resolve.firProvider
|
||||
import org.jetbrains.kotlin.fir.resolve.impl.FirProviderImpl
|
||||
import org.jetbrains.kotlin.fir.types.*
|
||||
import org.jetbrains.kotlin.fir.visitors.FirDefaultVisitorVoid
|
||||
import org.jetbrains.kotlin.fir.visitors.FirTransformer
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.sumByLong
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import kotlin.math.max
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.system.measureNanoTime
|
||||
|
||||
|
||||
fun checkFirProvidersConsistency(firFiles: List<FirFile>) {
|
||||
for ((session, files) in firFiles.groupBy { it.session }) {
|
||||
val provider = session.firProvider as FirProviderImpl
|
||||
provider.ensureConsistent(files)
|
||||
}
|
||||
}
|
||||
|
||||
private data class FailureInfo(val transformer: KClass<*>, val throwable: Throwable, val file: String)
|
||||
data class ErrorTypeReport(val report: String, var count: Int = 0)
|
||||
|
||||
class FirResolveBench(val withProgress: Boolean) {
|
||||
data class TotalStatistics(
|
||||
val unresolvedTypes: Int,
|
||||
val resolvedTypes: Int,
|
||||
val errorTypes: Int,
|
||||
val implicitTypes: Int,
|
||||
val errorFunctionCallTypes: Int,
|
||||
val errorQualifiedAccessTypes: Int,
|
||||
val fileCount: Int,
|
||||
val errorTypesReports: Map<String, ErrorTypeReport>,
|
||||
val timePerTransformer: Map<String, Measure>
|
||||
) {
|
||||
val totalTypes: Int = unresolvedTypes + resolvedTypes
|
||||
val goodTypes: Int = resolvedTypes - errorTypes - implicitTypes
|
||||
val uniqueErrorTypes: Int = errorTypesReports.size
|
||||
|
||||
val totalMeasure = Measure().apply {
|
||||
with(timePerTransformer.values) {
|
||||
time = sumByLong { it.time }
|
||||
user = sumByLong { it.user }
|
||||
cpu = sumByLong { it.cpu }
|
||||
gcTime = sumByLong { it.gcTime }
|
||||
gcCollections = sumBy { it.gcCollections }
|
||||
files = map { it.files }.average().toInt()
|
||||
}
|
||||
}
|
||||
|
||||
val totalTime: Long get() = totalMeasure.time
|
||||
}
|
||||
|
||||
data class Measure(
|
||||
var time: Long = 0,
|
||||
var user: Long = 0,
|
||||
var cpu: Long = 0,
|
||||
var gcTime: Long = 0,
|
||||
var gcCollections: Int = 0,
|
||||
var files: Int = 0
|
||||
)
|
||||
|
||||
val timePerTransformer = mutableMapOf<KClass<*>, Measure>()
|
||||
var resolvedTypes = 0
|
||||
var errorTypes = 0
|
||||
var unresolvedTypes = 0
|
||||
var errorFunctionCallTypes = 0
|
||||
var errorQualifiedAccessTypes = 0
|
||||
var implicitTypes = 0
|
||||
var fileCount = 0
|
||||
var totalTime = 0L
|
||||
|
||||
|
||||
private val fails = mutableListOf<FailureInfo>()
|
||||
val hasFiles get() = fails.isNotEmpty()
|
||||
|
||||
private val errorTypesReports = mutableMapOf<String, ErrorTypeReport>()
|
||||
|
||||
fun buildFiles(
|
||||
builder: RawFirBuilder,
|
||||
ktFiles: List<KtFile>
|
||||
): List<FirFile> {
|
||||
return ktFiles.map { file ->
|
||||
val before = vmStateSnapshot()
|
||||
var firFile: FirFile? = null
|
||||
val time = measureNanoTime {
|
||||
firFile = builder.buildFirFile(file)
|
||||
(builder.baseSession.firProvider as FirProviderImpl).recordFile(firFile!!)
|
||||
}
|
||||
val after = vmStateSnapshot()
|
||||
val diff = after - before
|
||||
recordTime(builder::class, diff, time)
|
||||
firFile!!
|
||||
}.also {
|
||||
totalTime = timePerTransformer.values.sumByLong { it.time }
|
||||
}
|
||||
}
|
||||
|
||||
fun buildFiles(
|
||||
builder: LightTree2Fir,
|
||||
files: List<File>
|
||||
): List<FirFile> {
|
||||
return files.map { file ->
|
||||
val before = vmStateSnapshot()
|
||||
var firFile: FirFile? = null
|
||||
val time = measureNanoTime {
|
||||
firFile = builder.buildFirFile(file)
|
||||
(builder.session.firProvider as FirProviderImpl).recordFile(firFile!!)
|
||||
}
|
||||
val after = vmStateSnapshot()
|
||||
val diff = after - before
|
||||
recordTime(builder::class, diff, time)
|
||||
firFile!!
|
||||
}.also {
|
||||
totalTime = timePerTransformer.values.sumByLong { it.time }
|
||||
}
|
||||
}
|
||||
|
||||
private fun recordTime(stageClass: KClass<*>, diff: VMCounters, time: Long) {
|
||||
timePerTransformer.computeIfAbsent(stageClass) { Measure() }.apply {
|
||||
this.time += time
|
||||
this.files += 1
|
||||
this.user += diff.userTime
|
||||
this.cpu += diff.cpuTime
|
||||
this.gcCollections += diff.gcInfo.values.sumBy { it.collections.toInt() }
|
||||
this.gcTime += diff.gcInfo.values.sumByLong { it.gcTime }
|
||||
}
|
||||
}
|
||||
|
||||
private fun runStage(transformer: FirTransformer<Nothing?>, firFileSequence: Sequence<FirFile>) {
|
||||
for (firFile in firFileSequence) {
|
||||
var fail = false
|
||||
val before = vmStateSnapshot()
|
||||
val time = measureNanoTime {
|
||||
try {
|
||||
transformer.transformFile(firFile, null)
|
||||
} catch (e: Throwable) {
|
||||
val ktFile = firFile.psi
|
||||
if (ktFile is KtFile) {
|
||||
println("Fail in file: ${ktFile.virtualFilePath}")
|
||||
fails += FailureInfo(transformer::class, e, ktFile.virtualFilePath)
|
||||
} else {
|
||||
println("Fail in file: ${firFile.packageFqName} / ${firFile.name}")
|
||||
fails += FailureInfo(transformer::class, e, firFile.packageFqName.asString() + "/" + firFile.name)
|
||||
}
|
||||
fail = true
|
||||
//println(ktFile.text)
|
||||
//throw e
|
||||
}
|
||||
}
|
||||
if (!fail) {
|
||||
val after = vmStateSnapshot()
|
||||
val diff = after - before
|
||||
recordTime(transformer::class, diff, time)
|
||||
|
||||
}
|
||||
//totalLength += StringBuilder().apply { FirRenderer(this).visitFile(firFile) }.length
|
||||
}
|
||||
}
|
||||
|
||||
fun processFiles(
|
||||
firFiles: List<FirFile>,
|
||||
transformers: List<FirTransformer<Nothing?>>
|
||||
) {
|
||||
fileCount += firFiles.size
|
||||
try {
|
||||
for ((stage, transformer) in transformers.withIndex()) {
|
||||
//println("Starting stage #$stage. $transformer")
|
||||
val firFileSequence = if (withProgress) firFiles.progress(" ~ ") else firFiles.asSequence()
|
||||
runStage(transformer, firFileSequence)
|
||||
checkFirProvidersConsistency(firFiles)
|
||||
}
|
||||
|
||||
if (fails.none()) {
|
||||
//println("SUCCESS!")
|
||||
} else {
|
||||
println("ERROR!")
|
||||
}
|
||||
} finally {
|
||||
|
||||
|
||||
val fileDocumentManager = FileDocumentManager.getInstance()
|
||||
|
||||
firFiles.forEach {
|
||||
it.accept(object : FirDefaultVisitorVoid() {
|
||||
|
||||
fun reportProblem(problem: String, psi: PsiElement) {
|
||||
val document = try {
|
||||
fileDocumentManager.getDocument(psi.containingFile.virtualFile)
|
||||
} catch (t: Throwable) {
|
||||
throw Exception("for file ${psi.containingFile}", t)
|
||||
}
|
||||
val line = (document?.getLineNumber(psi.startOffset) ?: 0)
|
||||
val char = psi.startOffset - (document?.getLineStartOffset(line) ?: 0)
|
||||
val report = "e: ${psi.containingFile?.virtualFile?.path}: (${line + 1}:$char): $problem"
|
||||
errorTypesReports.getOrPut(problem) { ErrorTypeReport(report) }.count++
|
||||
}
|
||||
|
||||
override fun visitElement(element: FirElement) {
|
||||
element.acceptChildren(this)
|
||||
}
|
||||
|
||||
override fun visitFunctionCall(functionCall: FirFunctionCall) {
|
||||
val typeRef = functionCall.typeRef
|
||||
val callee = functionCall.calleeReference
|
||||
if (typeRef is FirResolvedTypeRef) {
|
||||
val type = typeRef.type
|
||||
if (type is ConeKotlinErrorType) {
|
||||
errorFunctionCallTypes++
|
||||
val psi = callee.psi
|
||||
if (callee is FirErrorNamedReference && psi != null) {
|
||||
reportProblem(callee.diagnostic.reason, psi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitElement(functionCall)
|
||||
}
|
||||
|
||||
override fun visitQualifiedAccessExpression(qualifiedAccessExpression: FirQualifiedAccessExpression) {
|
||||
val typeRef = qualifiedAccessExpression.typeRef
|
||||
val callee = qualifiedAccessExpression.calleeReference
|
||||
if (typeRef is FirResolvedTypeRef) {
|
||||
val type = typeRef.type
|
||||
if (type is ConeKotlinErrorType) {
|
||||
errorQualifiedAccessTypes++
|
||||
val psi = callee.psi
|
||||
if (callee is FirErrorNamedReference && psi != null) {
|
||||
reportProblem(callee.diagnostic.reason, psi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitElement(qualifiedAccessExpression)
|
||||
}
|
||||
|
||||
override fun visitTypeRef(typeRef: FirTypeRef) {
|
||||
unresolvedTypes++
|
||||
|
||||
if (typeRef.psi != null) {
|
||||
if (typeRef is FirErrorTypeRef && typeRef.diagnostic is FirStubDiagnostic) {
|
||||
return
|
||||
}
|
||||
val psi = typeRef.psi!!
|
||||
val problem = "${typeRef::class.simpleName}: ${typeRef.render()}"
|
||||
reportProblem(problem, psi)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitImplicitTypeRef(implicitTypeRef: FirImplicitTypeRef) {
|
||||
visitTypeRef(implicitTypeRef)
|
||||
}
|
||||
|
||||
override fun visitComposedSuperTypeRef(composedSuperTypeRef: FirComposedSuperTypeRef) {}
|
||||
|
||||
override fun visitResolvedTypeRef(resolvedTypeRef: FirResolvedTypeRef) {
|
||||
resolvedTypes++
|
||||
val type = resolvedTypeRef.type
|
||||
if (type is ConeKotlinErrorType || type is ConeClassErrorType) {
|
||||
if (resolvedTypeRef.psi == null) {
|
||||
implicitTypes++
|
||||
} else {
|
||||
errorTypes++
|
||||
if (resolvedTypeRef is FirErrorTypeRef && resolvedTypeRef.diagnostic is FirStubDiagnostic) {
|
||||
return
|
||||
}
|
||||
val psi = resolvedTypeRef.psi!!
|
||||
val problem = "${resolvedTypeRef::class.simpleName} -> ${type::class.simpleName}: ${type.render()}"
|
||||
reportProblem(problem, psi)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun throwFailure() {
|
||||
if (fails.any()) {
|
||||
val (transformerClass, failure, file) = fails.first()
|
||||
throw AssertionError("Failures detected in ${transformerClass.simpleName}, file: $file", failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTotalStatistics(): TotalStatistics = TotalStatistics(
|
||||
unresolvedTypes,
|
||||
resolvedTypes,
|
||||
errorTypes,
|
||||
implicitTypes,
|
||||
errorFunctionCallTypes,
|
||||
errorQualifiedAccessTypes,
|
||||
fileCount,
|
||||
errorTypesReports,
|
||||
timePerTransformer.mapKeys { (klass, _) -> klass.simpleName!!.toString() }
|
||||
)
|
||||
}
|
||||
|
||||
fun doFirResolveTestBench(
|
||||
firFiles: List<FirFile>,
|
||||
transformers: List<FirTransformer<Nothing?>>,
|
||||
gc: Boolean = true,
|
||||
withProgress: Boolean = false,
|
||||
silent: Boolean = true
|
||||
) {
|
||||
|
||||
if (gc) {
|
||||
System.gc()
|
||||
}
|
||||
|
||||
val bench = FirResolveBench(withProgress)
|
||||
bench.processFiles(firFiles, transformers)
|
||||
if (!silent) bench.getTotalStatistics().report(System.out, "")
|
||||
bench.throwFailure()
|
||||
}
|
||||
|
||||
|
||||
fun <T> Collection<T>.progress(label: String, step: Double = 0.1): Sequence<T> {
|
||||
return progress(step) { label }
|
||||
}
|
||||
|
||||
fun <T> Collection<T>.progress(step: Double = 0.1, computeLabel: (T) -> String): Sequence<T> {
|
||||
val intStep = max(1, (this.size * step).toInt())
|
||||
var progress = 0
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
fun Long.formatTime(): String {
|
||||
return when {
|
||||
this < 1000 -> "${this}ms"
|
||||
this < 60 * 1000 -> "${this / 1000}s ${this % 1000}ms"
|
||||
else -> "${this / (60 * 1000)}m ${this % (60 * 1000) / 1000}s ${this % (60 * 1000) % 1000}ms"
|
||||
}
|
||||
}
|
||||
|
||||
return asSequence().onEach {
|
||||
if (progress % intStep == 0) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val elapsed = currentTime - startTime
|
||||
|
||||
val eta = if (progress > 0) ((elapsed / progress * 1.0) * (this.size - progress)).toLong().formatTime() else "Unknown"
|
||||
println("${computeLabel(it)}: ${progress * 100 / size}% ($progress/${this.size}), ETA: $eta, Elapsed: ${elapsed.formatTime()}")
|
||||
}
|
||||
progress++
|
||||
}
|
||||
}
|
||||
|
||||
fun FirResolveBench.TotalStatistics.reportErrors(stream: PrintStream) {
|
||||
errorTypesReports.values.sortedByDescending { it.count }.forEach {
|
||||
stream.print("${it.count}:")
|
||||
stream.println(it.report)
|
||||
}
|
||||
}
|
||||
|
||||
fun FirResolveBench.TotalStatistics.report(stream: PrintStream, header: String) {
|
||||
with(stream) {
|
||||
infix fun Int.percentOf(other: Int): String {
|
||||
return String.format("%.1f%%", this * 100.0 / other)
|
||||
}
|
||||
println()
|
||||
println("========== $header ==========")
|
||||
println("Unresolved (untouched) implicit types: $unresolvedTypes (${unresolvedTypes percentOf totalTypes})")
|
||||
println("Resolved types: $resolvedTypes (${resolvedTypes percentOf totalTypes})")
|
||||
println("Correctly resolved types: $goodTypes (${goodTypes percentOf resolvedTypes} of resolved)")
|
||||
println("Erroneously resolved types: $errorTypes (${errorTypes percentOf resolvedTypes} of resolved)")
|
||||
println(" - unresolved calls: $errorFunctionCallTypes")
|
||||
println(" - unresolved q.accesses: $errorQualifiedAccessTypes")
|
||||
println("Erroneously resolved implicit types: $implicitTypes (${implicitTypes percentOf resolvedTypes} of resolved)")
|
||||
println("Unique error types: $uniqueErrorTypes")
|
||||
|
||||
printTable(stream) {
|
||||
row {
|
||||
cell("Stage", LEFT)
|
||||
cells("Time", "Time per file", "Files: OK/E/T", "CPU", "User", "GC", "GC count")
|
||||
}
|
||||
separator()
|
||||
timePerTransformer.forEach { (transformer, measure) ->
|
||||
printMeasureAsTable(measure, this@report, transformer)
|
||||
}
|
||||
|
||||
if (timePerTransformer.keys.isNotEmpty()) {
|
||||
separator()
|
||||
printMeasureAsTable(totalMeasure, this@report, "Total time")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RTableContext.printMeasureAsTable(measure: FirResolveBench.Measure, statistics: FirResolveBench.TotalStatistics, label: String) {
|
||||
val time = measure.time
|
||||
val counter = measure.files
|
||||
row {
|
||||
cell(label, LEFT)
|
||||
timeCell(time, fractionDigits = 0)
|
||||
timeCell(time / counter)
|
||||
cell("$counter/${statistics.fileCount - counter}/${statistics.fileCount}")
|
||||
timeCell(measure.cpu, fractionDigits = 0)
|
||||
timeCell(measure.user)
|
||||
timeCell(measure.gcTime, inputUnit = TableTimeUnit.MS)
|
||||
cell(measure.gcCollections.toString())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2010-2020 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.fir
|
||||
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class RTableContext {
|
||||
val data: MutableList<Row> = mutableListOf()
|
||||
var cols = 0
|
||||
fun row(names: List<String>) = row(names.map { Cell(it) })
|
||||
|
||||
sealed class Row {
|
||||
class Data(val cells: List<Cell>) : Row()
|
||||
class Separator() : Row()
|
||||
}
|
||||
|
||||
@JvmName("rowCells")
|
||||
fun row(cells: List<Cell>) {
|
||||
cols = maxOf(cells.size, cols)
|
||||
data += Row.Data(cells)
|
||||
}
|
||||
|
||||
fun row(vararg names: String) {
|
||||
row(listOf(*names))
|
||||
}
|
||||
|
||||
fun separator() {
|
||||
data += Row.Separator()
|
||||
}
|
||||
|
||||
inner class RTableRowContext() {
|
||||
val rowData = mutableListOf<Cell>()
|
||||
val LEFT = false
|
||||
val RIGHT = true
|
||||
fun cell(text: String?, align: Boolean = RIGHT) {
|
||||
rowData += Cell(text.toString(), align)
|
||||
}
|
||||
|
||||
fun cells(texts: List<String>, align: Boolean = RIGHT) {
|
||||
rowData += texts.map { Cell(it, align) }
|
||||
}
|
||||
|
||||
fun cells(vararg texts: String, align: Boolean = RIGHT) {
|
||||
cells(listOf(*texts), align)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun row(body: RTableRowContext.() -> Unit) {
|
||||
val ctx = RTableRowContext()
|
||||
ctx.body()
|
||||
row(ctx.rowData)
|
||||
}
|
||||
|
||||
data class Cell(val text: String, val alignRight: Boolean = true) {
|
||||
fun padText(size: Int): String {
|
||||
return if (alignRight) {
|
||||
text.padStart(size)
|
||||
} else {
|
||||
text.padEnd(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun printout(out: Appendable) {
|
||||
val colSize = IntArray(cols) { index ->
|
||||
data.filterIsInstance<Row.Data>().fold(0) { acc, row -> maxOf(acc, row.cells.getOrNull(index)?.text?.length ?: 0) }
|
||||
}
|
||||
|
||||
|
||||
fun appendHLine(prefix: String, sep: String, postfix: String) {
|
||||
out.append(prefix)
|
||||
for ((index, size) in colSize.withIndex()) {
|
||||
if (index != 0) {
|
||||
out.append(sep)
|
||||
out.append(HLINE)
|
||||
}
|
||||
out.append(HLINE.repeat(size))
|
||||
}
|
||||
out.append(postfix)
|
||||
out.appendln()
|
||||
}
|
||||
|
||||
appendHLine(CORNER_LU, TOP_T, CORNER_RU)
|
||||
for (row in data) {
|
||||
when (row) {
|
||||
is Row.Data -> {
|
||||
out.append(VLINE)
|
||||
for ((index, cell) in row.cells.withIndex()) {
|
||||
out.append(cell.padText(colSize[index]))
|
||||
out.append(VLINE)
|
||||
out.append(" ")
|
||||
}
|
||||
out.appendln()
|
||||
}
|
||||
is Row.Separator -> {
|
||||
appendHLine(LEFT_T, CROSS, RIGHT_T)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
appendHLine(CORNER_LD, BOT_T, CORNER_RD)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CROSS = "┼"
|
||||
private const val VLINE = "│"
|
||||
private const val HLINE = "─"
|
||||
private const val CORNER_LU = "┌"
|
||||
private const val CORNER_RU = "┐"
|
||||
private const val CORNER_LD = "└"
|
||||
private const val CORNER_RD = "┘"
|
||||
private const val LEFT_T = "├"
|
||||
private const val RIGHT_T = "┤"
|
||||
private const val TOP_T = "┬"
|
||||
private const val BOT_T = "┴"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal enum class TableTimeUnit(val postfixText: String, val nsMultiplier: Double, val fractionDigits: Int) {
|
||||
NS("ns", 1.0, 0),
|
||||
MICS("mcs", 1e-3, 3),
|
||||
MS("ms", 1e-6, 6),
|
||||
S("s", 1e-9, 9);
|
||||
|
||||
fun convert(value: Long, from: TableTimeUnit): Double {
|
||||
return value / from.nsMultiplier * this.nsMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun RTableContext.RTableRowContext.timeCell(
|
||||
time: Long,
|
||||
outputUnit: TableTimeUnit = TableTimeUnit.MS,
|
||||
inputUnit: TableTimeUnit = TableTimeUnit.NS,
|
||||
fractionDigits: Int = outputUnit.fractionDigits
|
||||
) {
|
||||
val df = DecimalFormat()
|
||||
df.maximumFractionDigits = fractionDigits
|
||||
df.isGroupingUsed = true
|
||||
|
||||
cell("${df.format(outputUnit.convert(time, inputUnit))} ${outputUnit.postfixText}")
|
||||
}
|
||||
|
||||
internal inline fun printTable(out: Appendable = System.out, body: RTableContext.() -> Unit) {
|
||||
RTableContext().apply(body).printout(out)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2010-2020 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.fir
|
||||
|
||||
import org.jetbrains.kotlin.daemon.common.threadCpuTime
|
||||
import org.jetbrains.kotlin.daemon.common.threadUserTime
|
||||
import sun.management.ManagementFactoryHelper
|
||||
|
||||
|
||||
data class GCInfo(val name: String, val gcTime: Long, val collections: Long) {
|
||||
operator fun minus(other: GCInfo): GCInfo {
|
||||
return this.copy(
|
||||
gcTime = gcTime - other.gcTime,
|
||||
collections = collections - other.collections
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class VMCounters(val userTime: Long, val cpuTime: Long, val gcInfo: Map<String, GCInfo>) {
|
||||
|
||||
|
||||
operator fun minus(other: VMCounters): VMCounters {
|
||||
return VMCounters(
|
||||
userTime - other.userTime,
|
||||
cpuTime - other.cpuTime,
|
||||
merge(gcInfo, other.gcInfo) { a, b -> a - b }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun <K, V> merge(first: Map<K, V>, second: Map<K, V>, valueOp: (V, V) -> V): Map<K, V> {
|
||||
val result = first.toMutableMap()
|
||||
for ((k, v) in second) {
|
||||
result.merge(k, v, valueOp)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
object Init {
|
||||
init {
|
||||
ManagementFactoryHelper.getThreadMXBean().isThreadCpuTimeEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
fun vmStateSnapshot(): VMCounters {
|
||||
Init
|
||||
val threadMXBean = ManagementFactoryHelper.getThreadMXBean()
|
||||
|
||||
return VMCounters(
|
||||
threadMXBean.threadUserTime(), threadMXBean.threadCpuTime(),
|
||||
ManagementFactoryHelper.getGarbageCollectorMXBeans().associate { it.name to GCInfo(it.name, it.collectionTime, it.collectionCount) }
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user