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

Refactor DOT dump for Ets files #251

Merged
merged 22 commits into from
Aug 6, 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
3 changes: 3 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
# Builds on other branches will only read existing entries from the cache.
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}

- name: Set up GraphViz
uses: ts-graphviz/setup-graphviz@v2

- name: Set up Node
uses: actions/setup-node@v4
with:
Expand Down
4 changes: 3 additions & 1 deletion buildSrc/src/main/kotlin/Tests.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.testing.Test
import java.util.*
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import java.util.StringTokenizer

object Tests {
val lifecycleTag = "lifecycle"
Expand All @@ -10,6 +11,7 @@ object Tests {
fun Test.setup(jacocoTestReport: TaskProvider<*>) {
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = TestExceptionFormat.FULL
}
finalizedBy(jacocoTestReport) // report is always generated after tests run
val majorJavaVersion =
Expand Down
50 changes: 50 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Dot.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.jacodb.ets.utils

import mu.KotlinLogging
import java.nio.file.Path
import kotlin.io.path.absolute
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.io.path.nameWithoutExtension
import kotlin.time.Duration.Companion.seconds

private val logger = KotlinLogging.logger {}

fun render(
dotDir: Path,
relative: Path,
formats: List<String> = listOf("pdf"), // See: https://graphviz.org/docs/outputs/
) {
val dotPath = dotDir / relative
for (format in formats) {
logger.info { "Rendering DOT to ${format.uppercase()}..." }
val formatFile = (dotDir.resolveSibling(format) / relative)
.resolveSibling("${relative.nameWithoutExtension}.$format")
formatFile.parent?.createDirectories()
val cmd: List<String> = listOf(
"dot",
"-T$format",
"$dotPath",
"-o",
"$formatFile"
)
runProcess(cmd, 10.seconds)
logger.info { "Generated ${format.uppercase()} file: ${formatFile.absolute()}" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import org.jacodb.ets.dto.MethodDto
import org.jacodb.ets.dto.NopStmtDto
import org.jacodb.ets.dto.StmtDto
import org.jacodb.ets.dto.SwitchStmtDto
import java.io.BufferedWriter
import java.io.File
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.writeText

fun EtsFileDto.toDot(useLR: Boolean = false): String {
Expand Down Expand Up @@ -182,14 +184,15 @@ fun EtsFileDto.toDot(useLR: Boolean = false): String {
return lines.joinToString("\n")
}

fun EtsFileDto.dumpDot(file: File) {
file.writeText(toDot())
}

fun EtsFileDto.dumpDot(path: Path) {
path.parent?.createDirectories()
path.writeText(toDot())
}

fun EtsFileDto.dumpDot(file: File) {
dumpDot(file.toPath())
}

fun EtsFileDto.dumpDot(path: String) {
dumpDot(File(path))
}
11 changes: 7 additions & 4 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/EtsFileToDot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import org.jacodb.ets.base.EtsStmt
import org.jacodb.ets.model.EtsClass
import org.jacodb.ets.model.EtsFile
import org.jacodb.ets.model.EtsMethod
import java.io.BufferedWriter
import java.io.File
import java.nio.file.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.writeText

fun EtsFile.toDot(useLR: Boolean = false): String {
Expand Down Expand Up @@ -146,14 +148,15 @@ fun EtsFile.toDot(useLR: Boolean = false): String {
return lines.joinToString("\n")
}

fun EtsFile.dumpDot(file: File) {
file.writeText(toDot())
}

fun EtsFile.dumpDot(path: Path) {
path.parent?.createDirectories()
path.writeText(toDot())
}

fun EtsFile.dumpDot(file: File) {
dumpDot(file.toPath())
}

fun EtsFile.dumpDot(path: String) {
dumpDot(File(path))
}
76 changes: 46 additions & 30 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/LoadEtsFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@

package org.jacodb.ets.utils

import mu.KotlinLogging
import org.jacodb.ets.dto.EtsFileDto
import org.jacodb.ets.dto.convertToEtsFile
import org.jacodb.ets.model.EtsFile
import java.io.File
import java.io.FileNotFoundException
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.createTempDirectory
import kotlin.io.path.div
import kotlin.io.path.exists
import kotlin.io.path.inputStream
import kotlin.io.path.notExists
import kotlin.io.path.nameWithoutExtension
import kotlin.io.path.pathString

private val logger = KotlinLogging.logger {}
import kotlin.io.path.relativeTo
import kotlin.time.Duration.Companion.seconds

private const val ENV_VAR_ARK_ANALYZER_DIR = "ARKANALYZER_DIR"
private const val DEFAULT_ARK_ANALYZER_DIR = "arkanalyzer"
Expand All @@ -40,46 +40,62 @@ private const val DEFAULT_SERIALIZE_SCRIPT_PATH = "out/src/save/serializeArkIR.j
private const val ENV_VAR_NODE_EXECUTABLE = "NODE_EXECUTABLE"
private const val DEFAULT_NODE_EXECUTABLE = "node"

fun loadEtsFileAutoConvert(tsPath: String): EtsFile {
val arkAnalyzerDir = Paths.get(System.getenv(ENV_VAR_ARK_ANALYZER_DIR) ?: DEFAULT_ARK_ANALYZER_DIR)
fun generateEtsFileIR(tsPath: Path): Path {
val arkAnalyzerDir = Path(System.getenv(ENV_VAR_ARK_ANALYZER_DIR) ?: DEFAULT_ARK_ANALYZER_DIR)
if (!arkAnalyzerDir.exists()) {
throw FileNotFoundException("ArkAnalyzer directory does not exist: '$arkAnalyzerDir'. Did you forget to set the '$ENV_VAR_ARK_ANALYZER_DIR' environment variable? Current value is '${System.getenv(ENV_VAR_ARK_ANALYZER_DIR)}', current dir is '${Paths.get("").toAbsolutePath()}'.")
throw FileNotFoundException(
"ArkAnalyzer directory does not exist: '$arkAnalyzerDir'. " +
"Did you forget to set the '$ENV_VAR_ARK_ANALYZER_DIR' environment variable? " +
"Current value is '${System.getenv(ENV_VAR_ARK_ANALYZER_DIR)}', " +
"current dir is '${Path("").toAbsolutePath()}'."
)
}

val scriptPath = System.getenv(ENV_VAR_SERIALIZE_SCRIPT_PATH) ?: DEFAULT_SERIALIZE_SCRIPT_PATH
val script = arkAnalyzerDir.resolve(scriptPath)
if (!script.exists()) {
throw FileNotFoundException("Script file not found: '$script'. Did you forget to execute 'npm run build' in the arkanalyzer project?")
throw FileNotFoundException(
"Script file not found: '$script'. " +
"Did you forget to execute 'npm run build' in the arkanalyzer project?"
)
}

val node = System.getenv(ENV_VAR_NODE_EXECUTABLE) ?: DEFAULT_NODE_EXECUTABLE
val output = kotlin.io.path.createTempFile(prefix = File(tsPath).nameWithoutExtension + "_", suffix = ".json")
val output = kotlin.io.path.createTempFile(prefix = tsPath.nameWithoutExtension, suffix = ".json")
val cmd: List<String> = listOf(
node,
script.pathString,
tsPath,
tsPath.pathString,
output.pathString,
)
logger.info { "Running: '${cmd.joinToString(" ")}'" }
val process = ProcessBuilder(cmd).start()
val ok = process.waitFor(1, TimeUnit.MINUTES)
runProcess(cmd, 60.seconds)
return output
}

val stdout = process.inputStream.bufferedReader().readText().trim()
if (stdout.isNotBlank()) {
logger.info { "STDOUT:\n$stdout" }
}
val stderr = process.errorStream.bufferedReader().readText().trim()
if (stderr.isNotBlank()) {
logger.info { "STDERR:\n$stderr" }
fun loadEtsFileAutoConvert(tsPath: Path): EtsFile {
val irFilePath = generateEtsFileIR(tsPath)
irFilePath.inputStream().use { stream ->
val etsFileDto = EtsFileDto.loadFromJson(stream)
val etsFile = convertToEtsFile(etsFileDto)
return etsFile
}
}

if (!ok) {
logger.info { "Timeout!" }
process.destroy()
}
fun loadEtsFileAutoConvertWithDot(tsPath: Path): EtsFile {
val irPath = generateEtsFileIR(tsPath)
val dotDir = createTempDirectory(prefix = "dot_" + tsPath.nameWithoutExtension)

output.inputStream().use { stream ->
val etsFileDto = EtsFileDto.loadFromJson(stream)
return convertToEtsFile(etsFileDto)
val etsFileDto = irPath.inputStream().use { stream ->
EtsFileDto.loadFromJson(stream)
}
val etsFileDtoDotPath = dotDir / (tsPath.nameWithoutExtension + "_DTO.dot")
etsFileDto.dumpDot(etsFileDtoDotPath)
render(dotDir, etsFileDtoDotPath.relativeTo(dotDir))

val etsFile = convertToEtsFile(etsFileDto)
val etsFileDotPath = dotDir / (tsPath.nameWithoutExtension + ".dot")
etsFile.dumpDot(etsFileDotPath)
render(dotDir, etsFileDotPath.relativeTo(dotDir))

return etsFile
}
118 changes: 118 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2022 UnitTestBot contributors (utbot.org)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.jacodb.ets.utils

import mu.KotlinLogging
import org.jacodb.ets.dto.EtsFileDto
import org.jacodb.ets.model.EtsFile
import java.util.concurrent.TimeUnit
import kotlin.time.Duration

private val logger = KotlinLogging.logger {}

internal fun runProcess(cmd: List<String>, timeout: Duration? = null) {
logger.info { "Running: '${cmd.joinToString(" ")}'" }
val process = ProcessBuilder(cmd).start()
val ok = if (timeout == null) {
process.waitFor()
true
} else {
process.waitFor(timeout.inWholeNanoseconds, TimeUnit.NANOSECONDS)
}

val stdout = process.inputStream.bufferedReader().readText().trim()
if (stdout.isNotBlank()) {
logger.info { "STDOUT:\n$stdout" }
}
val stderr = process.errorStream.bufferedReader().readText().trim()
if (stderr.isNotBlank()) {
logger.info { "STDERR:\n$stderr" }
}

if (!ok) {
logger.info { "Timeout!" }
process.destroy()
}
}

fun EtsFileDto.toText(): String {
val lines: MutableList<String> = mutableListOf()
lines += "EtsFileDto '${name}':"
classes.forEach { clazz ->
lines += "= CLASS '${clazz.signature}':"
lines += " superClass = '${clazz.superClassName}'"
lines += " typeParameters = ${clazz.typeParameters}"
lines += " modifiers = ${clazz.modifiers}"
lines += " fields: ${clazz.fields.size}"
clazz.fields.forEach { field ->
lines += " - FIELD '${field.signature}'"
lines += " typeParameters = ${field.typeParameters}"
lines += " modifiers = ${field.modifiers}"
lines += " isOptional = ${field.isOptional}"
lines += " isDefinitelyAssigned = ${field.isDefinitelyAssigned}"
}
lines += " methods: ${clazz.methods.size}"
clazz.methods.forEach { method ->
lines += " - METHOD '${method.signature}'"
lines += " locals = ${method.body.locals}"
lines += " typeParameters = ${method.typeParameters}"
lines += " blocks: ${method.body.cfg.blocks.size}"
method.body.cfg.blocks.forEach { block ->
lines += " - BLOCK ${block.id}"
lines += " successors = ${block.successors}"
lines += " predecessors = ${block.predecessors}"
lines += " statements: ${block.stmts.size}"
block.stmts.forEachIndexed { i, stmt ->
lines += " ${i + 1}. $stmt"
}
}
}
}
return lines.joinToString("\n")
}

fun EtsFile.toText(): String {
val lines: MutableList<String> = mutableListOf()
lines += "EtsFile '${name}':"
classes.forEach { clazz ->
lines += "= CLASS '${clazz.signature}':"
lines += " superClass = '${clazz.superClass}'"
lines += " fields: ${clazz.fields.size}"
clazz.fields.forEach { field ->
lines += " - FIELD '${field.signature}'"
}
lines += " constructor = '${clazz.ctor.signature}'"
lines += " stmts: ${clazz.ctor.cfg.stmts.size}"
clazz.ctor.cfg.stmts.forEach { stmt ->
lines += " ${stmt.location.index}. $stmt"
val pad = " ".repeat("${stmt.location.index}".length + 2) // number + dot + space
lines += " ${pad}successors = ${clazz.ctor.cfg.successors(stmt).map { it.location.index }}"
}
lines += " methods: ${clazz.methods.size}"
clazz.methods.forEach { method ->
lines += " - METHOD '${method.signature}':"
lines += " locals = ${method.localsCount}"
lines += " stmts: ${method.cfg.stmts.size}"
method.cfg.stmts.forEach { stmt ->
lines += " ${stmt.location.index}. $stmt"
val pad = " ".repeat("${stmt.location.index}".length + 2) // number + dot + space
lines += " ${pad}successors = ${method.cfg.successors(stmt).map { it.location.index }}"
}
}
}
return lines.joinToString("\n")
}
Loading
Loading