Skip to content

Commit

Permalink
Added Java Decompilation to unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
LachlanMcKee committed Dec 29, 2020
1 parent da91b10 commit 59f637a
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 20 deletions.
1 change: 1 addition & 0 deletions debuglog-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0")
testImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable")
testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.2.6")
testImplementation("org.bitbucket.mstrobel:procyon-compilertools:0.5.36")
}

buildConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class DebugLogTransformer(
function: IrFunction
): IrCall {
val concat = irConcat()
concat.addArgument(irString(" ${function.name}("))
concat.addArgument(irString("-> ${function.name}("))
for ((index, valueParameter) in function.valueParameters.withIndex()) {
if (index > 0) concat.addArgument(irString(", "))
concat.addArgument(irString("${valueParameter.name}="))
Expand All @@ -131,7 +131,7 @@ class DebugLogTransformer(
result: IrExpression? = null
): IrCall {
val concat = irConcat()
concat.addArgument(irString(" ${function.name} ["))
concat.addArgument(irString("<- ${function.name} ["))
concat.addArgument(irCall(funElapsedNow).also { call ->
call.dispatchReceiver = irGet(startTime)
})
Expand Down
166 changes: 148 additions & 18 deletions debuglog-plugin/src/test/kotlin/com/bnorm/debug/log/IrPluginTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@

package com.bnorm.debug.log

import com.strobel.assembler.metadata.ArrayTypeLoader
import com.strobel.assembler.metadata.CompositeTypeLoader
import com.strobel.assembler.metadata.ITypeLoader
import com.strobel.decompiler.Decompiler
import com.strobel.decompiler.DecompilerSettings
import com.strobel.decompiler.PlainTextOutput
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
Expand All @@ -24,6 +30,7 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import java.io.StringWriter
import java.lang.reflect.InvocationTargetException

class IrPluginTest {
Expand All @@ -40,6 +47,9 @@ fun main() {
@DebugLog
fun greet(greeting: String = "Hello", name: String = "World"): String {
if (greeting != "Hello") {
return "Early return"
}
Thread.sleep(15)
return "${'$'}greeting, ${'$'}name!"
}
Expand All @@ -53,30 +63,128 @@ fun doSomething() {

@Test
fun `IR plugin enabled`() {
val result = compile(sourceFile = main, DebugLogComponentRegistrar(true))
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode)

val out = invokeMain(result, "MainKt").trim().split("\n")
assert(out.size == 8)
assert(out[0] == "⇢ greet(greeting=Hello, name=World)")
assert(out[1].matches("⇠ greet \\[\\d+(\\.\\d+)?ms] = Hello, World!".toRegex()))
assert(out[2] == "Hello, World!")
assert(out[3] == "⇢ greet(greeting=Hello, name=Kotlin IR)")
assert(out[4].matches("⇠ greet \\[\\d+(\\.\\d+)?ms] = Hello, Kotlin IR!".toRegex()))
assert(out[5] == "Hello, Kotlin IR!")
assert(out[6] == "⇢ doSomething()")
assert(out[7].matches("⇠ doSomething \\[\\d+(\\.\\d+)?ms]".toRegex()))
testIrPlugin(
isEnabled = true,
expectedGreetMethod = """
public static final String greet(@NotNull final String greeting, @NotNull final String name) {
Intrinsics.checkNotNullParameter((Object)greeting, "greeting");
Intrinsics.checkNotNullParameter((Object)name, "name");
System.out.println((Object)("-> greet(greeting=" + greeting + ", name=" + name + ')'));
final TimeMark markNow = TimeSource${'$'}Monotonic.INSTANCE.markNow();
try {
if (!Intrinsics.areEqual((Object)greeting, "Hello")) {
System.out.println((Object)new StringBuilder().append("<- greet [").append((Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc())).append("] = ").append("Early return").toString());
return "Early return";
}
Thread.sleep(15L);
final String string = greeting + ", " + name + '!';
System.out.println((Object)new StringBuilder().append("<- greet [").append((Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc())).append("] = ").append(string).toString());
return string;
}
catch (Throwable t) {
System.out.println((Object)new StringBuilder().append("<- greet [").append((Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc())).append("] = ").append((Object)t).toString());
throw t;
}
}
""".trimIndent(),

expectedDoSomethingMethod = """
public static final void doSomething() {
System.out.println("-> doSomething()");
final TimeMark markNow = TimeSource${'$'}Monotonic.INSTANCE.markNow();
try {
Thread.sleep(15L);
System.out.println((Object)new StringBuilder().append("<- doSomething [").append((Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc())).append(']').toString());
}
catch (Throwable t) {
System.out.println((Object)new StringBuilder().append("<- doSomething [").append((Object)Duration.toString-impl(markNow.elapsedNow-UwyO8pc())).append("] = ").append((Object)t).toString());
throw t;
}
}
""".trimIndent(),

outputVerifyFunc = { out ->
assert(out.size == 8)
assert(out[0] == "-> greet(greeting=Hello, name=World)")
assert(out[1].matches("<- greet \\[\\d+(\\.\\d+)?ms] = Hello, World!".toRegex()))
assert(out[2] == "Hello, World!")
assert(out[3] == "-> greet(greeting=Hello, name=Kotlin IR)")
assert(out[4].matches("<- greet \\[\\d+(\\.\\d+)?ms] = Hello, Kotlin IR!".toRegex()))
assert(out[5] == "Hello, Kotlin IR!")
assert(out[6] == "-> doSomething()")
assert(out[7].matches("<- doSomething \\[\\d+(\\.\\d+)?ms]".toRegex()))
}
)
}

@Test
fun `IR plugin disabled`() {
val result = compile(sourceFile = main, DebugLogComponentRegistrar(false))
testIrPlugin(
isEnabled = false,
expectedGreetMethod = """
public static final String greet(@NotNull final String greeting, @NotNull final String name) {
Intrinsics.checkNotNullParameter((Object)greeting, "greeting");
Intrinsics.checkNotNullParameter((Object)name, "name");
if (!Intrinsics.areEqual((Object)greeting, "Hello")) {
return "Early return";
}
Thread.sleep(15L);
return greeting + ", " + name + '!';
}
""".trimIndent(),

expectedDoSomethingMethod = """
public static final void doSomething() {
Thread.sleep(15L);
}
""".trimIndent(),

outputVerifyFunc = { out ->
assertTrue(out.size == 2)
assertEquals("Hello, World!", out[0])
assertEquals("Hello, Kotlin IR!", out[1])
}
)
}

private fun testIrPlugin(
isEnabled: Boolean,
expectedGreetMethod: String,
expectedDoSomethingMethod: String,
outputVerifyFunc: (List<String>) -> Unit
) {
val result = compile(sourceFile = main, DebugLogComponentRegistrar(isEnabled))
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode)

val out = invokeMain(result, "MainKt").trim().split("\n")
assertTrue(out.size == 2)
assertTrue(out[0] == "Hello, World!")
assertTrue(out[1] == "Hello, Kotlin IR!")
val mainJavaCode = result.javaCode("MainKt")
assertEquals(expectedGreetMethod, methodGetter(mainJavaCode, "public static final String greet"))
assertEquals(expectedDoSomethingMethod, methodGetter(mainJavaCode, "public static final void doSomething"))

outputVerifyFunc(invokeMain(result, "MainKt").trim().split(System.lineSeparator()))
}

private fun methodGetter(classText: String, methodPrefix: String): String {
val lines = classText.split("\n")
val firstLineIndex = lines.indexOfFirst { it.contains(methodPrefix) }
val indentationCount = lines[firstLineIndex].takeWhile { it == ' ' }.length

var curleyBraceCount = 1
var currentIndex: Int = firstLineIndex + 1

while (curleyBraceCount != 0 && currentIndex < lines.lastIndex) {
if (lines[currentIndex].contains("{")) {
curleyBraceCount++
}
if (lines[currentIndex].contains("}")) {
curleyBraceCount--
}

currentIndex++
}

return lines
.subList(firstLineIndex, currentIndex)
.joinToString("\n") { it.substring(indentationCount) }
}
}

Expand Down Expand Up @@ -119,3 +227,25 @@ fun invokeMain(result: KotlinCompilation.Result, className: String): String {
System.setOut(oldOut)
}
}

fun KotlinCompilation.Result.javaCode(className: String): String {
val decompilerSettings = DecompilerSettings.javaDefaults().apply {
typeLoader = CompositeTypeLoader(*(mutableListOf<ITypeLoader>()
.apply {
// Add every class to the type loaders to ensure it's everything gets loaded correctly.
generatedFiles.forEach {
add(ArrayTypeLoader(it.readBytes()))
}
}
.toTypedArray()))
}

return StringWriter().let { writer ->
Decompiler.decompile(
className,
PlainTextOutput(writer),
decompilerSettings
)
writer.toString().trimEnd().trimIndent()
}
}

0 comments on commit 59f637a

Please sign in to comment.