Skip to content

Commit

Permalink
Add a Test Framework For Parsers (#46)
Browse files Browse the repository at this point in the history
Also include a failing test for #45

Will fix it in the next cl.
  • Loading branch information
alonalbert authored May 9, 2024
1 parent 168be46 commit 95259b8
Show file tree
Hide file tree
Showing 11 changed files with 1,532 additions and 46 deletions.
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
!**/src/**/build/

### IntelliJ IDEA ###
.idea/modules.xml
Expand Down Expand Up @@ -40,4 +39,5 @@ bin/
.vscode/

### Mac OS ###
.DS_Store
.DS_Store
/local.properties
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ kotlin {
val jvmTest by getting {
dependencies {
implementation(libs.junit4)
implementation(libs.kotlin.test)
implementation(libs.truth)
}
}
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jewel = { group = "org.jetbrains.jewel", name = "jewel-int-ui-standalone-241", v
jewel-decorated = { group = "org.jetbrains.jewel", name = "jewel-int-ui-decorated-window-241", version.ref = "jewel" }
jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
lifecycle = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycle" }
lifecycle-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycle" }
Expand Down
49 changes: 7 additions & 42 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package dev.romainguy.kotlin.explorer

import dev.romainguy.kotlin.explorer.build.ByteCodeDecompiler
import dev.romainguy.kotlin.explorer.build.KotlinCompiler
import dev.romainguy.kotlin.explorer.bytecode.ByteCodeParser
import dev.romainguy.kotlin.explorer.code.CodeContent
import dev.romainguy.kotlin.explorer.dex.DexDumpParser
Expand All @@ -33,6 +35,7 @@ private const val TotalDisassemblySteps = 6
private const val TotalRunSteps = 2
private const val Done = 1f

private val byteCodeDecompiler = ByteCodeDecompiler()
private val byteCodeParser = ByteCodeParser()
private val dexDumpParser = DexDumpParser()
private val oatDumpParser = OatDumpParser()
Expand All @@ -56,10 +59,7 @@ suspend fun buildAndRun(
val path = directory.resolve("KotlinExplorer.kt")
Files.writeString(path, source)

val kotlinc = process(
*buildKotlincCommand(toolPaths, path),
directory = directory
)
val kotlinc = KotlinCompiler(toolPaths, directory).compile(path)

if (kotlinc.exitCode != 0) {
launch(ui) {
Expand Down Expand Up @@ -114,10 +114,7 @@ suspend fun buildAndDisassemble(
val path = directory.resolve("KotlinExplorer.kt")
Files.writeString(path, source)

val kotlinc = process(
*buildKotlincCommand(toolPaths, path),
directory = directory
)
val kotlinc = KotlinCompiler(toolPaths, directory).compile(path)

if (kotlinc.exitCode != 0) {
launch(ui) {
Expand All @@ -129,11 +126,7 @@ suspend fun buildAndDisassemble(

launch(ui) { onStatusUpdate("Disassembling ByteCode…", step++ / TotalDisassemblySteps) }

val javap = process(
*buildJavapCommand(directory),
directory = directory
)

val javap = byteCodeDecompiler.decompile(directory)
launch { onByteCode(byteCodeParser.parse(javap.output)) }

if (javap.exitCode != 0) {
Expand Down Expand Up @@ -249,18 +242,6 @@ private fun buildJavaCommand(toolPaths: ToolPaths): Array<String> {
return command.toTypedArray()
}

private fun buildJavapCommand(directory: Path): Array<String> {
val command = mutableListOf("javap", "-p", "-l", "-c")
val classFiles = Files
.list(directory)
.filter { path -> path.extension == "class" }
.map { file -> file.fileName.toString() }
.sorted()
.collect(Collectors.toList())
command.addAll(classFiles)
return command.toTypedArray()
}

private fun buildR8Command(
toolPaths: ToolPaths,
directory: Path,
Expand Down Expand Up @@ -318,22 +299,6 @@ private fun buildR8Command(
return command.toTypedArray()
}

private fun buildKotlincCommand(toolPaths: ToolPaths, path: Path): Array<String> {
val command = mutableListOf(
toolPaths.kotlinc.toString(),
path.toString(),
"-Xmulti-platform",
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions",
"-classpath",
toolPaths.kotlinLibs.joinToString(":") { jar -> jar.toString() }
+ ":${toolPaths.platform}"
)

return command.toTypedArray()
}

private fun writeR8Rules(directory: Path) {
// Match $ANDROID_HOME/tools/proguard/proguard-android-optimize.txt
Files.writeString(
Expand All @@ -343,7 +308,7 @@ private fun writeR8Rules(directory: Path) {
-allowaccessmodification
-dontpreverify
-dontobfuscate
-keep,allowoptimization class !kotlin.**,!kotlinx.** {
-keep,allow optimization class !kotlin.**,!kotlinx.** {
<methods>;
}""".trimIndent()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 Romain Guy
*
* 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 dev.romainguy.kotlin.explorer.build

import dev.romainguy.kotlin.explorer.process
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.extension
import kotlin.io.path.pathString
import kotlin.io.path.walk

class ByteCodeDecompiler {
suspend fun decompile(directory: Path) = process(*buildJavapCommand(directory), directory = directory)

@OptIn(ExperimentalPathApi::class)
private fun buildJavapCommand(directory: Path): Array<String> {
val command = mutableListOf("javap", "-p", "-l", "-c")
val classFiles = directory.walk()
.filter { path -> path.extension == "class" }
.map { path -> directory.relativize(path).pathString }
.sorted()
command.addAll(classFiles)
return command.toTypedArray()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2024 Romain Guy
*
* 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 dev.romainguy.kotlin.explorer.build

import dev.romainguy.kotlin.explorer.ToolPaths
import dev.romainguy.kotlin.explorer.process
import java.nio.file.Path

class KotlinCompiler(private val toolPaths: ToolPaths, private val outputDirectory: Path) {

suspend fun compile(source: Path) = process(*buildCompileCommand(source), directory = outputDirectory)

private fun buildCompileCommand(file: Path): Array<String> {
val command = mutableListOf(
toolPaths.kotlinc.toString(),
file.toString(),
"-Xmulti-platform",
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions",
"-classpath",
(toolPaths.kotlinLibs + toolPaths.platform).joinToString(":") { jar -> jar.toString() }
)

return command.toTypedArray()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import dev.romainguy.kotlin.explorer.getValue
* public final class KotlinExplorerKt {
* ```
*/
private val ClassRegex = Regex("^.* class [_a-zA-Z][_\\w]+ \\{$")
private val ClassRegex = Regex("^.* class [_a-zA-Z][_\\w.]+ \\{$")

/**
* Examples:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 Romain Guy
*
* 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 dev.romainguy.kotlin.explorer.bytecode

import dev.romainguy.kotlin.explorer.testing.Builder
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder

class ByteCodeParserTest {
@get:Rule
val temporaryFolder = TemporaryFolder()

private val builder by lazy { Builder.getInstance(temporaryFolder.root.toPath()) }
private val byteCodeParser = ByteCodeParser()

@Test
fun issue_45() {
val byteCode = builder.generateByteCode("Issue_45.kt")
val content = byteCodeParser.parse(byteCode)
println(content)
}
}
129 changes: 129 additions & 0 deletions src/jvmTest/kotlin/dev/romainguy/kotlin/explorer/testing/Builder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright (C) 2024 Romain Guy
*
* 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 dev.romainguy.kotlin.explorer.testing

import dev.romainguy.kotlin.explorer.ToolPaths
import dev.romainguy.kotlin.explorer.build.ByteCodeDecompiler
import dev.romainguy.kotlin.explorer.build.KotlinCompiler
import kotlinx.coroutines.runBlocking
import java.nio.file.Path
import java.util.*
import kotlin.io.path.*
import kotlin.test.fail

interface Builder {
fun generateByteCode(testFile: String): String

companion object {
fun getInstance(outputDirectory: Path) =
try {
LocalBuilder(outputDirectory)
} catch (e: Throwable) {
System.err.println("Failed to create local builder. Using Github builder")
GithubBuilder()
}
}
}

class GithubBuilder : Builder {
private val cwd = Path.of(System.getProperty("user.dir"))
private val testData = cwd.resolve("src/jvmTest/kotlin/testData")

override fun generateByteCode(testFile: String): String {
return testData.resolve(testFile.replace(".kt", ".javap")).readText()
}
}

class LocalBuilder(private val outputDirectory: Path) : Builder {
private val cwd = Path.of(System.getProperty("user.dir"))
private val testData = cwd.resolve("src/jvmTest/kotlin/testData")
private val toolPaths = createToolsPath()
private val kotlinCompiler = KotlinCompiler(toolPaths, outputDirectory)
private val byteCodeDecompiler = ByteCodeDecompiler()

override fun generateByteCode(testFile: String): String {
val path = testData.resolve(testFile)
if (path.notExists()) {
fail("$path does not exists")
}
return runBlocking {
kotlinCompile(path)
val result = byteCodeDecompiler.decompile(outputDirectory)
if (result.exitCode != 0) {
System.err.println(result.output)
fail("javap error")
}

// Save the fine under <project/build/testData> so we can examine it when needed
val saveFile = testData.resolve(testFile.replace(".kt", ".javap"))
if (saveFile.parent.notExists()) {
saveFile.parent.createDirectory()
}
saveFile.writeText(result.output)

result.output
}
}

private suspend fun kotlinCompile(path: Path) {
val result = kotlinCompiler.compile(path)
if (result.exitCode != 0) {
System.err.println(result.output)
fail("kotlinc error")
}
}

private fun createToolsPath(): ToolPaths {
val properties = Properties()
properties.load(cwd.resolve("local.properties").reader())

val kotlinHome = getKotlinHome(properties)
val androidHome = getAndroidHome(properties)
val toolPaths = ToolPaths(Path.of(""), androidHome, kotlinHome)
if (toolPaths.isKotlinHomeValid && toolPaths.isAndroidHomeValid) {
return toolPaths
}
throw IllegalStateException("Invalid ToolsPath: KOTLIN_HOME=${kotlinHome} ANDROID_HOME=$androidHome")
}
}

private fun getKotlinHome(properties: Properties): Path {
val pathString = System.getenv("KOTLIN_HOME") ?: properties.getProperty("kotlin.home")

if (pathString == null) {
throw IllegalStateException("Could not find Android SDK")
}
val path = Path.of(pathString)
if (path.notExists()) {
throw IllegalStateException("Could not find Android SDK")
}
return path
}

private fun getAndroidHome(properties: Properties): Path {
val path =
when (val androidHome: String? = System.getenv("ANDROID_HOME") ?: properties.getProperty("android.home")) {
null -> Path.of(System.getProperty("user.home")).resolve("Android/Sdk")
else -> Path.of(androidHome)
}

if (path.notExists()) {
throw IllegalStateException("Could not find Android SDK")
}
return path
}

Loading

0 comments on commit 95259b8

Please sign in to comment.