Diagnostic tests are logging their lazy activity
This commit is contained in:
@@ -41,6 +41,7 @@ import com.intellij.util.Function;
|
||||
import com.intellij.util.Processor;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import junit.framework.TestCase;
|
||||
import kotlin.Function1;
|
||||
import kotlin.KotlinPackage;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -438,6 +439,15 @@ public class JetTestUtils {
|
||||
}
|
||||
|
||||
public static void assertEqualsToFile(@NotNull File expectedFile, @NotNull String actual) {
|
||||
assertEqualsToFile(expectedFile, actual, new Function1<String, String>() {
|
||||
@Override
|
||||
public String invoke(String s) {
|
||||
return s;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void assertEqualsToFile(@NotNull File expectedFile, @NotNull String actual, @NotNull Function1<String, String> sanitizer) {
|
||||
try {
|
||||
String actualText = UtilPackage.trimTrailingWhitespacesAndAddNewlineAtEOF(StringUtil.convertLineSeparators(actual.trim()));
|
||||
|
||||
@@ -449,7 +459,7 @@ public class JetTestUtils {
|
||||
|
||||
String expectedText = UtilPackage.trimTrailingWhitespacesAndAddNewlineAtEOF(StringUtil.convertLineSeparators(expected.trim()));
|
||||
|
||||
if (!Comparing.equal(expectedText, actualText)) {
|
||||
if (!Comparing.equal(sanitizer.invoke(expectedText), sanitizer.invoke(actualText))) {
|
||||
throw new FileComparisonFailure("Actual data differs from file content: " + expectedFile.getName(),
|
||||
expected, actual, expectedFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
@@ -26,8 +26,11 @@ import com.intellij.psi.PsiFile;
|
||||
import kotlin.Function1;
|
||||
import kotlin.KotlinPackage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.jet.JetTestUtils;
|
||||
import org.jetbrains.jet.cli.jvm.compiler.CliLightClassGenerationSupport;
|
||||
import org.jetbrains.jet.context.GlobalContext;
|
||||
import org.jetbrains.jet.context.SimpleGlobalContext;
|
||||
import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor;
|
||||
import org.jetbrains.jet.lang.descriptors.PackageFragmentDescriptor;
|
||||
import org.jetbrains.jet.lang.descriptors.PackageFragmentProvider;
|
||||
@@ -46,6 +49,8 @@ import org.jetbrains.jet.lang.resolve.lazy.LazyResolveTestUtil;
|
||||
import org.jetbrains.jet.lang.resolve.name.FqName;
|
||||
import org.jetbrains.jet.lang.resolve.name.Name;
|
||||
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns;
|
||||
import org.jetbrains.jet.storage.ExceptionTracker;
|
||||
import org.jetbrains.jet.storage.LockBasedStorageManager;
|
||||
import org.jetbrains.jet.test.util.DescriptorValidator;
|
||||
import org.jetbrains.jet.test.util.RecursiveDescriptorComparator;
|
||||
import org.jetbrains.jet.utils.UtilsPackage;
|
||||
@@ -57,6 +62,14 @@ import static org.jetbrains.jet.lang.diagnostics.Errors.*;
|
||||
import static org.jetbrains.jet.test.util.RecursiveDescriptorComparator.RECURSIVE;
|
||||
|
||||
public abstract class AbstractJetDiagnosticsTest extends BaseDiagnosticsTest {
|
||||
|
||||
public static final Function1<String, String> HASH_SANITIZER = new Function1<String, String>() {
|
||||
@Override
|
||||
public String invoke(String s) {
|
||||
return s.replaceAll("@(\\d)+", "");
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void analyzeAndCheck(File testDataFile, List<TestFile> testFiles) {
|
||||
Map<TestModule, List<TestFile>> groupedByModule = KotlinPackage.groupByTo(
|
||||
@@ -77,6 +90,8 @@ public abstract class AbstractJetDiagnosticsTest extends BaseDiagnosticsTest {
|
||||
Map<TestModule, ModuleDescriptorImpl> modules = createModules(groupedByModule);
|
||||
Map<TestModule, BindingContext> moduleBindings = new HashMap<TestModule, BindingContext>();
|
||||
|
||||
LazyOperationsLog lazyOperationsLog = new LazyOperationsLog(HASH_SANITIZER);
|
||||
|
||||
for (Map.Entry<TestModule, List<TestFile>> entry : groupedByModule.entrySet()) {
|
||||
TestModule testModule = entry.getKey();
|
||||
List<? extends TestFile> testFilesInModule = entry.getValue();
|
||||
@@ -101,8 +116,18 @@ public abstract class AbstractJetDiagnosticsTest extends BaseDiagnosticsTest {
|
||||
|
||||
// New JavaDescriptorResolver is created for each module, which is good because it emulates different Java libraries for each module,
|
||||
// albeit with same class names
|
||||
TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
|
||||
ExceptionTracker tracker = new ExceptionTracker();
|
||||
GlobalContext context = new SimpleGlobalContext(
|
||||
new LoggingStorageManager(
|
||||
LockBasedStorageManager.createWithExceptionHandling(tracker),
|
||||
lazyOperationsLog.getAddRecordFunction()
|
||||
),
|
||||
tracker
|
||||
);
|
||||
|
||||
TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegrationWithCustomContext(
|
||||
getProject(),
|
||||
context,
|
||||
jetFiles,
|
||||
moduleTrace,
|
||||
Predicates.<PsiFile>alwaysTrue(),
|
||||
@@ -116,6 +141,8 @@ public abstract class AbstractJetDiagnosticsTest extends BaseDiagnosticsTest {
|
||||
|
||||
// We want to always create a test data file (txt) if it was missing,
|
||||
// but don't want to skip the following checks in case this one fails
|
||||
Throwable exceptionFromLazyResolveLogValidation = checkLazyResolveLog(lazyOperationsLog, testDataFile);
|
||||
|
||||
Throwable exceptionFromDescriptorValidation = null;
|
||||
try {
|
||||
File expectedFile = new File(FileUtil.getNameWithoutExtension(testDataFile.getAbsolutePath()) + ".txt");
|
||||
@@ -143,6 +170,27 @@ public abstract class AbstractJetDiagnosticsTest extends BaseDiagnosticsTest {
|
||||
if (exceptionFromDescriptorValidation != null) {
|
||||
throw UtilsPackage.rethrow(exceptionFromDescriptorValidation);
|
||||
}
|
||||
if (exceptionFromLazyResolveLogValidation != null) {
|
||||
throw UtilsPackage.rethrow(exceptionFromLazyResolveLogValidation);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Throwable checkLazyResolveLog(LazyOperationsLog lazyOperationsLog, File testDataFile) {
|
||||
Throwable exceptionFromLazyResolveLogValidation = null;
|
||||
try {
|
||||
File expectedFile = new File(FileUtil.getNameWithoutExtension(testDataFile.getAbsolutePath()) + ".lazy.log");
|
||||
|
||||
JetTestUtils.assertEqualsToFile(
|
||||
expectedFile,
|
||||
lazyOperationsLog.getText(),
|
||||
HASH_SANITIZER
|
||||
);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
exceptionFromLazyResolveLogValidation = e;
|
||||
}
|
||||
return exceptionFromLazyResolveLogValidation;
|
||||
}
|
||||
|
||||
private void validateAndCompareDescriptorWithFile(
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2010-2014 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.jet.checkers
|
||||
|
||||
import org.jetbrains.jet.lang.resolve.name.FqName
|
||||
import org.jetbrains.jet.lang.resolve.name.Name
|
||||
import org.jetbrains.jet.lang.descriptors.Named
|
||||
import java.util.IdentityHashMap
|
||||
import org.jetbrains.jet.lang.resolve.scopes.JetScope
|
||||
import org.jetbrains.jet.lang.resolve.java.structure.impl.JavaTypeImpl
|
||||
import org.jetbrains.jet.lang.resolve.java.structure.impl.JavaClassImpl
|
||||
import java.util.ArrayList
|
||||
import org.jetbrains.jet.utils.Printer
|
||||
import org.jetbrains.jet.lang.resolve.java.structure.JavaNamedElement
|
||||
import org.jetbrains.jet.descriptors.serialization.ProtoBuf
|
||||
import org.jetbrains.jet.descriptors.serialization.TypeDeserializer
|
||||
import org.jetbrains.jet.descriptors.serialization.context.DeserializationContext
|
||||
import org.jetbrains.jet.lang.types.JetType
|
||||
import org.jetbrains.jet.lang.resolve.DescriptorUtils
|
||||
import java.util.HashMap
|
||||
import org.jetbrains.jet.lang.types.JetTypeImpl
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class LazyOperationsLog(
|
||||
val stringSanitizer: (String) -> String
|
||||
) {
|
||||
val ids = IdentityHashMap<Any, Int>()
|
||||
private fun objectId(o: Any): Int = ids.getOrPut(o, { ids.size() })
|
||||
|
||||
private class Record(
|
||||
val lambda: Any,
|
||||
val data: LoggingStorageManager.CallData
|
||||
)
|
||||
|
||||
private val records = ArrayList<Record>()
|
||||
|
||||
public val addRecordFunction: (lambda: Any, LoggingStorageManager.CallData) -> Unit = {
|
||||
lambda, data ->
|
||||
records.add(Record(lambda, data))
|
||||
}
|
||||
|
||||
public fun getText(): String {
|
||||
val groupedByOwner = records.groupByTo(IdentityHashMap()) {
|
||||
val owner = it.data.fieldOwner
|
||||
if (owner is JetScope) owner.getContainingDeclaration() else owner
|
||||
}.map { Pair(it.getKey(), it.getValue()) }
|
||||
|
||||
return groupedByOwner.map {
|
||||
val (owner, records) = it
|
||||
renderOwner(owner, records)
|
||||
}.sortBy(stringSanitizer).join("\n").renumberObjects()
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces ids in the given string so that they increase
|
||||
* Example:
|
||||
* input = "A@21 B@6"
|
||||
* output = "A@0 B@1"
|
||||
*/
|
||||
private fun String.renumberObjects(): String {
|
||||
val ids = HashMap<String, String>()
|
||||
fun newId(objectId: String): String {
|
||||
return ids.getOrPut(objectId, { "@" + ids.size() })
|
||||
}
|
||||
|
||||
val m = Pattern.compile("@\\d+").matcher(this)
|
||||
val sb = StringBuffer()
|
||||
while (m.find()) {
|
||||
m.appendReplacement(sb, newId(m.group(0)))
|
||||
}
|
||||
m.appendTail(sb)
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun renderOwner(owner: Any?, records: List<Record>): String {
|
||||
val sb = StringBuilder()
|
||||
with (Printer(sb)) {
|
||||
println(render(owner), " {")
|
||||
indent {
|
||||
records.map { renderRecord(it) }.sortBy(stringSanitizer).forEach {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
println("}")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun renderRecord(record: Record): String {
|
||||
val data = record.data
|
||||
val sb = StringBuilder()
|
||||
|
||||
sb.append(data.field?.getName() ?: "<name not found>")
|
||||
|
||||
if (!data.arguments.isEmpty()) {
|
||||
sb.append(data.arguments.map { render(it) }.join(", ", "(", ")"))
|
||||
}
|
||||
|
||||
sb.append(" = ${render(data.result)}")
|
||||
|
||||
if (data.fieldOwner is JetScope) {
|
||||
sb.append(" // through ${render(data.fieldOwner)}")
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun render(o: Any?): String {
|
||||
if (o == null) return "null"
|
||||
|
||||
val sb = StringBuilder()
|
||||
if (o is FqName || o is Name || o is String || o is Number || o is Boolean) {
|
||||
sb.append("'$o': ")
|
||||
}
|
||||
|
||||
val id = objectId(o)
|
||||
|
||||
val aClass = o.javaClass
|
||||
sb.append(if (aClass.isAnonymousClass()) aClass.getName().substringAfterLast('.') else aClass.getSimpleName()).append("@$id")
|
||||
when {
|
||||
o is Named -> sb.append("['${o.getName()}']")
|
||||
o.javaClass.getSimpleName() == "LazyJavaClassifierType" -> {
|
||||
val javaType = o.field<JavaTypeImpl<*>>("javaType")
|
||||
sb.append("['${javaType.getPsi().getPresentableText()}']")
|
||||
}
|
||||
o.javaClass.getSimpleName() == "LazyJavaClassTypeConstructor" -> {
|
||||
val javaClass = o.field<Any>("this\$0").field<JavaClassImpl>("jClass")
|
||||
sb.append("['${javaClass.getPsi().getName()}']")
|
||||
}
|
||||
o.javaClass.getSimpleName() == "DeserializedType" -> {
|
||||
val typeDeserializer = o.field<TypeDeserializer>("this\$0")
|
||||
val context = typeDeserializer.field<DeserializationContext>("context")
|
||||
val typeProto = o.field<ProtoBuf.Type>("typeProto")
|
||||
val text = when (typeProto.getConstructor().getKind()) {
|
||||
ProtoBuf.Type.Constructor.Kind.CLASS -> context.nameResolver.getFqName(typeProto.getConstructor().getId()).asString()
|
||||
ProtoBuf.Type.Constructor.Kind.TYPE_PARAMETER -> {
|
||||
val classifier = (o as JetType).getConstructor().getDeclarationDescriptor()
|
||||
"" + classifier.getName() + " in " + DescriptorUtils.getFqName(classifier.getContainingDeclaration())
|
||||
}
|
||||
else -> "???"
|
||||
}
|
||||
sb.append("['$text']")
|
||||
}
|
||||
o is JavaNamedElement -> {
|
||||
sb.append("['${o.getName()}']")
|
||||
}
|
||||
o is JavaTypeImpl<*> -> {
|
||||
sb.append("['${o.getPsi().getPresentableText()}']")
|
||||
}
|
||||
o is Collection<*> -> {
|
||||
if (o.isEmpty()) {
|
||||
sb.append("[empty]")
|
||||
}
|
||||
else {
|
||||
val size = o.size()
|
||||
sb.append("[$size] { ").append(o.take(3).map { render(it) }.join(", "))
|
||||
if (o.size() > 3) sb.append(", ...")
|
||||
sb.append(" }")
|
||||
}
|
||||
}
|
||||
o is JetTypeImpl -> {
|
||||
sb.append("['").append(o.getConstructor())
|
||||
if (!o.getArguments().isEmpty()) {
|
||||
sb.append("<${o.getArguments().size()}>")
|
||||
}
|
||||
sb.append("']")
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Any.field(name: String): T {
|
||||
val field = this.javaClass.getDeclaredField(name)
|
||||
field.setAccessible(true)
|
||||
[suppress("UNCHECKED_CAST")]
|
||||
return field.get(this) as T
|
||||
}
|
||||
|
||||
private fun Printer.indent(body: Printer.() -> Unit): Printer {
|
||||
pushIndent()
|
||||
body()
|
||||
popIndent()
|
||||
return this
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2010-2014 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jetbrains.jet.checkers
|
||||
|
||||
import org.jetbrains.jet.checkers.LoggingStorageManager.CallData
|
||||
import org.jetbrains.jet.storage.StorageManager
|
||||
import java.lang.reflect.*
|
||||
import org.jetbrains.jet.storage.MemoizedFunctionToNotNull
|
||||
import org.jetbrains.jet.storage.MemoizedFunctionToNullable
|
||||
import org.jetbrains.jet.storage.NotNullLazyValue
|
||||
import org.jetbrains.jet.storage.NullableLazyValue
|
||||
|
||||
public class LoggingStorageManager(
|
||||
private val delegate: StorageManager,
|
||||
private val callHandler: (lambda: Any, call: LoggingStorageManager.CallData?) -> Unit
|
||||
) : StorageManager {
|
||||
|
||||
public class CallData(
|
||||
val fieldOwner: Any?,
|
||||
val field: Field?,
|
||||
val lambdaCreatedIn: GenericDeclaration?,
|
||||
val arguments: List<Any?>,
|
||||
val result: Any?
|
||||
)
|
||||
|
||||
// Creating objects here because we need a reference to it
|
||||
private val <T> (() -> T).logged: () -> T
|
||||
get() = object : () -> T {
|
||||
override fun invoke(): T {
|
||||
val result = this@logged()
|
||||
callHandler(this@logged, computeCallerData(this@logged, this, listOf(), result))
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Creating objects here because we need a reference to it
|
||||
private val <K, V> ((K) -> V).logged: (K) -> V
|
||||
get() = object : (K) -> V {
|
||||
override fun invoke(p1: K): V {
|
||||
val result = this@logged(p1)
|
||||
callHandler(this@logged, computeCallerData(this@logged, this, listOf(p1), result))
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeCallerData(lambda: Any, wrapper: Any, arguments: List<Any?>, result: Any?): CallData {
|
||||
val jClass = lambda.javaClass
|
||||
|
||||
val outerClass: Class<out Any?>? = jClass.getEnclosingClass()
|
||||
|
||||
// fields named "this" or "this$0"
|
||||
val referenceToOuter = jClass.getAllDeclaredFields().firstOrNull {
|
||||
field ->
|
||||
field.getType() == outerClass && field.getName()!!.contains("this")
|
||||
}
|
||||
referenceToOuter?.setAccessible(true)
|
||||
|
||||
val outerInstance = referenceToOuter?.get(lambda)
|
||||
|
||||
val containingField = if (outerInstance == null) null
|
||||
else outerClass?.getAllDeclaredFields()?.firstOrNull {
|
||||
(field): Boolean ->
|
||||
field.setAccessible(true)
|
||||
val value = field.get(outerInstance)
|
||||
if (value == null) return@firstOrNull false
|
||||
|
||||
val valueClass = value.javaClass
|
||||
|
||||
val functionField = valueClass.getAllDeclaredFields().firstOrNull {
|
||||
it.getType()?.getName()?.startsWith("kotlin.Function") ?: false
|
||||
}
|
||||
if (functionField == null) return@firstOrNull false
|
||||
|
||||
functionField.setAccessible(true)
|
||||
val functionValue = functionField.get(value)
|
||||
functionValue == wrapper
|
||||
}
|
||||
|
||||
val enclosingEntity = jClass.getEnclosingConstructor()
|
||||
?: jClass.getEnclosingMethod()
|
||||
?: jClass.getEnclosingClass()
|
||||
|
||||
return CallData(outerInstance, containingField, enclosingEntity as GenericDeclaration?, arguments, result)
|
||||
}
|
||||
|
||||
private fun Class<*>.getAllDeclaredFields(): List<Field> {
|
||||
val result = arrayListOf<Field>()
|
||||
|
||||
var c = this
|
||||
while (true) {
|
||||
result.addAll(c.getDeclaredFields().toList())
|
||||
[suppress("UNCHECKED_CAST")]
|
||||
val superClass = (c as Class<Any>).getSuperclass() as Class<Any>?
|
||||
if (superClass == null) break
|
||||
if (c == superClass) break
|
||||
c = superClass
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun createMemoizedFunction<K, V: Any>(compute: (K) -> V): MemoizedFunctionToNotNull<K, V> {
|
||||
return delegate.createMemoizedFunction(compute.logged)
|
||||
}
|
||||
|
||||
override fun createMemoizedFunctionWithNullableValues<K, V: Any>(compute: (K) -> V?): MemoizedFunctionToNullable<K, V> {
|
||||
return delegate.createMemoizedFunctionWithNullableValues(compute.logged)
|
||||
}
|
||||
|
||||
override fun createLazyValue<T: Any>(computable: () -> T): NotNullLazyValue<T> {
|
||||
return delegate.createLazyValue(computable.logged)
|
||||
}
|
||||
|
||||
override fun createRecursionTolerantLazyValue<T: Any>(computable: () -> T, onRecursiveCall: T): NotNullLazyValue<T> {
|
||||
return delegate.createRecursionTolerantLazyValue(computable.logged, onRecursiveCall)
|
||||
}
|
||||
|
||||
override fun createLazyValueWithPostCompute<T: Any>(computable: () -> T, onRecursiveCall: ((Boolean) -> T)?, postCompute: (T) -> Unit): NotNullLazyValue<T> {
|
||||
return delegate.createLazyValueWithPostCompute(computable.logged, onRecursiveCall, postCompute)
|
||||
}
|
||||
|
||||
override fun createNullableLazyValue<T: Any>(computable: () -> T?): NullableLazyValue<T> {
|
||||
return delegate.createNullableLazyValue(computable.logged)
|
||||
}
|
||||
|
||||
override fun createRecursionTolerantNullableLazyValue<T: Any>(computable: () -> T?, onRecursiveCall: T?): NullableLazyValue<T> {
|
||||
return delegate.createRecursionTolerantNullableLazyValue(computable.logged, onRecursiveCall)
|
||||
}
|
||||
|
||||
override fun createNullableLazyValueWithPostCompute<T: Any>(computable: () -> T?, postCompute: (T?) -> Unit): NullableLazyValue<T> {
|
||||
return delegate.createNullableLazyValueWithPostCompute(computable.logged, postCompute)
|
||||
}
|
||||
|
||||
override fun compute<T>(computable: () -> T): T {
|
||||
return delegate.compute(computable.logged)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user