From 0a4860b4d72c39a4150f01c9dbfb0e029ae1cc56 Mon Sep 17 00:00:00 2001 From: Simon Ogorodnik Date: Wed, 25 Oct 2017 01:09:02 +0300 Subject: [PATCH] Add instrumentation to locate not cleared MockApplication --- buildSrc/src/main/kotlin/tasks.kt | 10 ++ .../test/testFramework/KtUsefulTestCase.java | 5 + .../MockComponentManagerCreationTracer.kt | 48 +++++++ settings.gradle | 3 +- test-instrumenter/build.gradle.kts | 42 ++++++ ...kApplicationCreationTracingInstrumenter.kt | 135 ++++++++++++++++++ .../testFramework/TestInstrumentationAgent.kt | 34 +++++ 7 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 compiler/tests-common/tests/org/jetbrains/kotlin/testFramework/MockComponentManagerCreationTracer.kt create mode 100644 test-instrumenter/build.gradle.kts create mode 100644 test-instrumenter/src/org/jetbrains/kotlin/testFramework/MockApplicationCreationTracingInstrumenter.kt create mode 100644 test-instrumenter/src/org/jetbrains/kotlin/testFramework/TestInstrumentationAgent.kt diff --git a/buildSrc/src/main/kotlin/tasks.kt b/buildSrc/src/main/kotlin/tasks.kt index ba50797579d..602bce4c448 100644 --- a/buildSrc/src/main/kotlin/tasks.kt +++ b/buildSrc/src/main/kotlin/tasks.kt @@ -40,6 +40,16 @@ fun Project.projectTest(taskName: String = "test", body: Test.() -> Unit = {}): } } + doFirst { + val agent = tasks.findByPath(":test-instrumenter:jar")!!.outputs.files.singleFile + + val args = project.findProperty("kotlin.test.instrumentation.args")?.let { "=$it" }.orEmpty() + + jvmArgs("-javaagent:$agent$args") + } + + dependsOn(":test-instrumenter:jar") + jvmArgs("-ea", "-XX:+HeapDumpOnOutOfMemoryError", "-Xmx1100m", "-XX:+UseCodeCacheFlushing", "-XX:ReservedCodeCacheSize=128m", "-Djna.nosys=true") maxHeapSize = "1100m" systemProperty("idea.is.unit.test", "true") diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/test/testFramework/KtUsefulTestCase.java b/compiler/tests-common/tests/org/jetbrains/kotlin/test/testFramework/KtUsefulTestCase.java index d293cba626a..e46292cc55f 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/test/testFramework/KtUsefulTestCase.java +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/test/testFramework/KtUsefulTestCase.java @@ -40,6 +40,7 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.kotlin.testFramework.MockComponentManagerCreationTracer; import org.jetbrains.kotlin.types.FlexibleTypeImpl; import org.jetbrains.kotlin.utils.ExceptionUtilsKt; import org.junit.Assert; @@ -88,6 +89,10 @@ public abstract class KtUsefulTestCase extends TestCase { protected void setUp() throws Exception { application = ApplicationManager.getApplication(); + if (application != null && application.isDisposed()) { + MockComponentManagerCreationTracer.diagnoseDisposedButNotClearedApplication(application); + } + super.setUp(); String testName = FileUtil.sanitizeFileName(getTestName(true)); diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/testFramework/MockComponentManagerCreationTracer.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/testFramework/MockComponentManagerCreationTracer.kt new file mode 100644 index 00000000000..60cf1a2b0a0 --- /dev/null +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/testFramework/MockComponentManagerCreationTracer.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2017 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.kotlin.testFramework + +import com.intellij.mock.MockComponentManager +import com.intellij.openapi.application.Application +import com.intellij.util.containers.ContainerUtil +import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase + +object MockComponentManagerCreationTracer { + + private val creationTraceMap = ContainerUtil.createConcurrentWeakMap() + + @JvmStatic + fun onCreate(manager: MockComponentManager) { + creationTraceMap[manager] = Exception("Creation trace") + } + + @JvmStatic + fun onGetComponentInstance(manager: MockComponentManager) { + if (manager.isDisposed) { + val trace = creationTraceMap[manager] ?: return + trace.printStackTrace(System.err) + } + } + + @JvmStatic + fun diagnoseDisposedButNotClearedApplication(app: Application) { + if (app is MockComponentManager) { + KtUsefulTestCase.resetApplicationToNull() + throw IllegalStateException("Some test disposed, but forgot to clear MockApplication", creationTraceMap[app]) + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index e439955b575..e8480917341 100644 --- a/settings.gradle +++ b/settings.gradle @@ -142,7 +142,8 @@ include ":kotlin-build-common", // plugin markers: ':kotlin-gradle-plugin:plugin-marker', ':kotlin-allopen:plugin-marker', - ':kotlin-noarg:plugin-marker' + ':kotlin-noarg:plugin-marker', + ":test-instrumenter" rootProject.name = "kotlin" diff --git a/test-instrumenter/build.gradle.kts b/test-instrumenter/build.gradle.kts new file mode 100644 index 00000000000..ad0ec9fc0d6 --- /dev/null +++ b/test-instrumenter/build.gradle.kts @@ -0,0 +1,42 @@ +import org.gradle.jvm.tasks.Jar + +/* + * Copyright 2010-2017 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. + */ +plugins { java } +apply { plugin("kotlin") } + + + +dependencies { + compile(ideaSdkCoreDeps("intellij-core")) + compile(ideaSdkDeps("asm-all")) + compile(project(":kotlin-stdlib")) +} + + +sourceSets { + "main" { projectDefault() } +} + +tasks { + "jar" { + this as Jar + manifest { + attributes["Manifest-Version"] = 1.0 + attributes["PreMain-Class"] = "org.jetbrains.kotlin.testFramework.TestInstrumentationAgent" + } + } +} \ No newline at end of file diff --git a/test-instrumenter/src/org/jetbrains/kotlin/testFramework/MockApplicationCreationTracingInstrumenter.kt b/test-instrumenter/src/org/jetbrains/kotlin/testFramework/MockApplicationCreationTracingInstrumenter.kt new file mode 100644 index 00000000000..73027b853d9 --- /dev/null +++ b/test-instrumenter/src/org/jetbrains/kotlin/testFramework/MockApplicationCreationTracingInstrumenter.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2010-2017 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.kotlin.testFramework + +import org.jetbrains.org.objectweb.asm.* +import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor +import java.io.PrintWriter +import java.lang.instrument.ClassFileTransformer +import java.security.ProtectionDomain + + +class MockApplicationCreationTracingInstrumenter(private val debugInfo: Boolean) : ClassFileTransformer { + + private fun loadTransformAndSerialize(classfileBuffer: ByteArray, lambda: (out: ClassVisitor) -> ClassVisitor): ByteArray { + val reader = ClassReader(classfileBuffer) + + val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + + val pv = if (debugInfo) { + TraceClassVisitor(writer, PrintWriter(System.out.writer())) + } + else { + writer + } + + reader.accept(lambda(pv), 0) + + return writer.toByteArray() + } + + private fun isMockComponentManagerCreationTracerCanBeLoaded(loader: ClassLoader): Boolean = + loader.getResource("org/jetbrains/kotlin/testFramework/MockComponentManagerCreationTracer.class") != null + + override fun transform( + loader: ClassLoader, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray + ): ByteArray? { + if (loader::class.java.name == "org.jetbrains.kotlin.preloading.MemoryBasedClassLoader") return null + + if (className == "com/intellij/mock/MockComponentManager" && isMockComponentManagerCreationTracerCanBeLoaded(loader)) { + return loadTransformAndSerialize(classfileBuffer, this::transformMockComponentManager) + } + else if (className == "com/intellij/mock/MockComponentManager$1" && isMockComponentManagerCreationTracerCanBeLoaded(loader)) { + return loadTransformAndSerialize(classfileBuffer, this::transformMockComponentManagerPicoContainer) + } + + return null + } + + + private fun createMethodTransformClassVisitor( + out: ClassVisitor, + predicate: (name: String, desc: String) -> Boolean, + transform: (original: MethodVisitor) -> MethodVisitor + ): ClassVisitor { + return object : ClassVisitor(Opcodes.ASM6, out) { + + var visited = false + + override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array?): MethodVisitor { + val original = super.visitMethod(access, name, desc, signature, exceptions) + return if (predicate(name, desc)) { + assert(!visited) + visited = true + transform(original) + } + else { + original + } + } + + override fun visitEnd() { + super.visitEnd() + assert(visited) + } + } + } + + private fun transformMockComponentManagerPicoContainer(out: ClassVisitor): ClassVisitor { + return createMethodTransformClassVisitor(out, { name, _ -> name == "getComponentInstance" }) { original -> + object : MethodVisitor(Opcodes.ASM6, original) { + override fun visitCode() { + super.visitCode() + visitLabel(Label()) + visitVarInsn(Opcodes.ALOAD, 0) + visitFieldInsn(Opcodes.GETFIELD, "com/intellij/mock/MockComponentManager$1", "this$0", "Lcom/intellij/mock/MockComponentManager;") + visitMethodInsn( + Opcodes.INVOKESTATIC, + "org/jetbrains/kotlin/testFramework/MockComponentManagerCreationTracer", + "onGetComponentInstance", + "(Lcom/intellij/mock/MockComponentManager;)V", + false + ) + } + } + } + } + + private fun transformMockComponentManager(out: ClassVisitor): ClassVisitor { + return createMethodTransformClassVisitor(out, { name, _ -> name == "" }) { original -> + object : MethodVisitor(Opcodes.ASM6, original) { + override fun visitInsn(opcode: Int) { + if (opcode == Opcodes.RETURN) { + visitVarInsn(Opcodes.ALOAD, 0) + visitMethodInsn( + Opcodes.INVOKESTATIC, + "org/jetbrains/kotlin/testFramework/MockComponentManagerCreationTracer", + "onCreate", + "(Lcom/intellij/mock/MockComponentManager;)V", + false + ) + } + super.visitInsn(opcode) + } + } + } + } +} diff --git a/test-instrumenter/src/org/jetbrains/kotlin/testFramework/TestInstrumentationAgent.kt b/test-instrumenter/src/org/jetbrains/kotlin/testFramework/TestInstrumentationAgent.kt new file mode 100644 index 00000000000..e6e5d3f664b --- /dev/null +++ b/test-instrumenter/src/org/jetbrains/kotlin/testFramework/TestInstrumentationAgent.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2017 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.kotlin.testFramework + +import java.lang.instrument.Instrumentation + +@Suppress("unused") +object TestInstrumentationAgent { + @JvmStatic + fun premain(arg: String?, instrumentation: Instrumentation) { + + val arguments = arg.orEmpty().split(",") + + val debug = "debug" in arguments + if (debug) { + println("org.jetbrains.kotlin.testFramework.TestInstrumentationAgent: premain") + } + instrumentation.addTransformer(MockApplicationCreationTracingInstrumenter(debug)) + } +} \ No newline at end of file