Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Test Framework For Parsers #46

Merged
merged 1 commit into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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