Skip to content

Commit

Permalink
parse aptos command line json output (#237)
Browse files Browse the repository at this point in the history
* add aptos result json parser

* remove stdIn from execute(_)

* parse aptos command line json output

* handle jackson exception
  • Loading branch information
mkurnikov authored Nov 10, 2024
1 parent a46ed54 commit 4688ed3
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 60 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ allprojects {
}
// cannot be updated further, problems with underlying library
implementation("com.github.ajalt.clikt:clikt:3.5.4")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.+")

testImplementation("junit:junit:4.13.2")
testImplementation("org.opentest4j:opentest4j:1.3.0")
Expand Down
43 changes: 31 additions & 12 deletions src/main/kotlin/org/move/cli/MoveProjectsSyncTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.move.cli.MoveProject.UpdateStatus
import org.move.cli.manifest.MoveToml
import org.move.cli.runConfigurations.aptos.AptosExitStatus
import org.move.cli.settings.getAptosCli
import org.move.cli.settings.moveSettings
import org.move.lang.toNioPathOrNull
Expand Down Expand Up @@ -182,7 +183,6 @@ class MoveProjectsSyncTask(
}

private fun fetchDependencyPackages(childContext: SyncContext, projectRoot: Path): TaskResult<Unit> {
val taskResult: TaskResult<Unit>? = null
val syncListener =
SyncProcessListener(childContext) { event ->
childContext.syncProgress.output(event.text, true)
Expand All @@ -192,17 +192,36 @@ class MoveProjectsSyncTask(
return when {
aptos == null -> TaskResult.Err("Invalid Aptos CLI configuration")
else -> {
aptos.fetchPackageDependencies(
projectRoot,
skipLatest,
processListener = syncListener
).unwrapOrElse {
return TaskResult.Err(
"Failed to fetch / update dependencies",
it.message
)
val aptosProcessOutput =
aptos.fetchPackageDependencies(
projectRoot,
skipLatest,
processListener = syncListener
).unwrapOrElse {
return TaskResult.Err(
"Failed to fetch / update dependencies",
it.message
)
}
when (val exitStatus = aptosProcessOutput.exitStatus) {
is AptosExitStatus.Result -> TaskResult.Ok(Unit)
is AptosExitStatus.Error -> {
if (exitStatus.message.contains("Unable to resolve packages for package")) {
return TaskResult.Err(
"Unable to resolve packages",
exitStatus.message.split(": ").joinToString(": \n")
)
}
if (!exitStatus.message.contains("Compilation error")) {
return TaskResult.Err(
"Error occurred",
exitStatus.message.split(": ").joinToString(": \n")
)
}
TaskResult.Ok(Unit)
}
is AptosExitStatus.Malformed -> TaskResult.Ok(Unit)
}
TaskResult.Ok(Unit)
}
}
}
Expand Down Expand Up @@ -310,7 +329,7 @@ class MoveProjectsSyncTask(
val depRoot = dep.rootDirectory()
.unwrapOrElse {
syncContext.syncProgress.message(
"Failed to load ${dep.name}.",
"Failed to load ${dep.name}",
"Error when resolving dependency root, \n${it.message}",
MessageEvent.Kind.ERROR,
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ data class AptosCommandLine(
.withParameters(this.arguments)
.withWorkingDirectory(this.workingDirectory)
.withCharset(Charsets.UTF_8)
// disables default coloring for stderr
.withRedirectErrorStream(true)
this.environmentVariables.configureCommandLine(generalCommandLine, true)
return generalCommandLine
}
Expand Down
79 changes: 38 additions & 41 deletions src/main/kotlin/org/move/cli/runConfigurations/aptos/Aptos.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.move.cli.runConfigurations.aptos

import com.fasterxml.jackson.core.JacksonException
import com.intellij.execution.configuration.EnvironmentVariablesData
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessListener
Expand All @@ -9,12 +10,9 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.execution.ParametersListUtil
import org.move.cli.MvConstants
import org.move.cli.MoveProject
import org.move.cli.externalLinter.ExternalLinter
import org.move.cli.externalLinter.externalLinterSettings
import org.move.cli.MvConstants
import org.move.cli.runConfigurations.AptosCommandLine
import org.move.cli.settings.moveSettings
import org.move.openapiext.*
import org.move.openapiext.common.isUnitTestMode
import org.move.stdext.RsResult
Expand Down Expand Up @@ -56,17 +54,17 @@ data class Aptos(val cliLocation: Path, val parentDisposable: Disposable?): Disp
executeCommandLine(commandLine).unwrapOrElse { return Err(it) }

fullyRefreshDirectory(rootDirectory)

val manifest =
checkNotNull(rootDirectory.findChild(MvConstants.MANIFEST_FILE)) { "Can't find the manifest file" }

return Ok(manifest)
}

fun fetchPackageDependencies(
projectDir: Path,
skipLatest: Boolean,
processListener: ProcessListener
): RsProcessResult<Unit> {
): AptosProcessResult<Unit> {
val commandLine =
AptosCommandLine(
subCommand = "move compile",
Expand All @@ -75,10 +73,7 @@ data class Aptos(val cliLocation: Path, val parentDisposable: Disposable?): Disp
),
workingDirectory = projectDir
)
commandLine
.toColoredCommandLine(this.cliLocation)
.execute(innerDisposable, listener = processListener)
return Ok(Unit)
return executeAptosCommandLine(commandLine, colored = true, listener = processListener)
}

fun checkProject(args: AptosCompileArgs): RsResult<ProcessOutput, RsProcessExecutionException.Start> {
Expand Down Expand Up @@ -179,44 +174,46 @@ data class Aptos(val cliLocation: Path, val parentDisposable: Disposable?): Disp

private fun executeCommandLine(
commandLine: AptosCommandLine,
colored: Boolean = false,
listener: ProcessListener? = null,
runner: CapturingProcessHandler.() -> ProcessOutput = { runProcessWithGlobalProgress() }
): RsProcessResult<ProcessOutput> {
return commandLine
.toGeneralCommandLine(this.cliLocation)
.execute(innerDisposable, stdIn = null, listener = listener, runner = runner)
val generalCommandLine = if (colored) {
commandLine.toColoredCommandLine(this.cliLocation)
} else {
commandLine.toGeneralCommandLine(this.cliLocation)
}
return generalCommandLine.execute(innerDisposable, runner, listener)
}

override fun dispose() {}
}

data class AptosCompileArgs(
val linter: ExternalLinter,
val moveProjectDirectory: Path,
val extraArguments: String,
val envs: Map<String, String>,
val enableMove2: Boolean,
val skipLatestGitDeps: Boolean,
) {
companion object {
fun forMoveProject(moveProject: MoveProject): AptosCompileArgs {
val linterSettings = moveProject.project.externalLinterSettings
val moveSettings = moveProject.project.moveSettings

val additionalArguments = linterSettings.additionalArguments
val enviroment = linterSettings.envs
val workingDirectory = moveProject.workingDirectory

return AptosCompileArgs(
linterSettings.tool,
workingDirectory,
additionalArguments,
enviroment,
enableMove2 = moveSettings.enableMove2,
skipLatestGitDeps = moveSettings.skipFetchLatestGitDeps
)
private fun executeAptosCommandLine(
commandLine: AptosCommandLine,
colored: Boolean = false,
listener: ProcessListener? = null,
runner: CapturingProcessHandler.() -> ProcessOutput = { runProcessWithGlobalProgress() }
): AptosProcessResult<Unit> {
val processOutput = executeCommandLine(commandLine, colored, listener, runner)
.unwrapOrElse {
if (it !is RsProcessExecutionException.FailedWithNonZeroExitCode) {
return Err(it)
}
it.output
}

val json = processOutput.stdout
.lines().dropWhile { l -> !l.startsWith("{") }.joinToString("\n").trim()
val exitStatus = try {
AptosExitStatus.fromJson(json)
} catch (e: JacksonException) {
return Err(RsDeserializationException(e))
}
val aptosProcessOutput = AptosProcessOutput(Unit, processOutput, exitStatus)

return Ok(aptosProcessOutput)
}

override fun dispose() {}
}


val MoveProject.workingDirectory: Path get() = this.currentPackage.contentRoot.pathAsPath
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.move.cli.runConfigurations.aptos

import org.move.cli.MoveProject
import org.move.cli.externalLinter.ExternalLinter
import org.move.cli.externalLinter.externalLinterSettings
import org.move.cli.settings.moveSettings
import java.nio.file.Path

data class AptosCompileArgs(
val linter: ExternalLinter,
val moveProjectDirectory: Path,
val extraArguments: String,
val envs: Map<String, String>,
val enableMove2: Boolean,
val skipLatestGitDeps: Boolean,
) {
companion object {
fun forMoveProject(moveProject: MoveProject): AptosCompileArgs {
val linterSettings = moveProject.project.externalLinterSettings
val moveSettings = moveProject.project.moveSettings

val additionalArguments = linterSettings.additionalArguments
val enviroment = linterSettings.envs
val workingDirectory = moveProject.workingDirectory

return AptosCompileArgs(
linterSettings.tool,
workingDirectory,
additionalArguments,
enviroment,
enableMove2 = moveSettings.enableMove2,
skipLatestGitDeps = moveSettings.skipFetchLatestGitDeps
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.move.cli.runConfigurations.aptos

import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.intellij.lang.annotations.Language

sealed class AptosExitStatus(val message: String) {
class Result(message: String): AptosExitStatus(message)
class Error(message: String): AptosExitStatus(message)
class Malformed(jsonText: String): AptosExitStatus(jsonText)

companion object {
@Throws(JacksonException::class)
fun fromJson(@Language("JSON") json: String): AptosExitStatus {
val parsedResult = JSON_MAPPER.readValue(json, AptosJsonResult::class.java)
return when {
parsedResult.Error != null -> Error(parsedResult.Error)
parsedResult.Result != null -> Result(parsedResult.Result)
else -> Malformed(json)
}
}
}
}

@Suppress("PropertyName")
private data class AptosJsonResult(
val Result: String?,
val Error: String?
)

private val JSON_MAPPER: ObjectMapper = ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerKotlinModule()
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.move.cli.runConfigurations.aptos

import com.intellij.execution.process.ProcessOutput
import org.move.openapiext.RsProcessExecutionOrDeserializationException
import org.move.stdext.RsResult

typealias AptosProcessResult<T> = RsResult<AptosProcessOutput<T>, RsProcessExecutionOrDeserializationException>

data class AptosProcessOutput<T>(
val item: T,
val output: ProcessOutput,
val exitStatus: AptosExitStatus,
) {
fun <T, U> replaceItem(newItem: U): AptosProcessOutput<U> = AptosProcessOutput(newItem, output, exitStatus)
}
6 changes: 1 addition & 5 deletions src/main/kotlin/org/move/openapiext/CommandLineExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ fun GeneralCommandLine.execute(): ProcessOutput? {
/// `owner` parameter represents the object whose lifetime it's using for the process lifetime
fun GeneralCommandLine.execute(
owner: CheckedDisposable,
stdIn: ByteArray? = null,
runner: CapturingProcessHandler.() -> ProcessOutput = { runProcessWithGlobalProgress() },
listener: ProcessListener? = null
): RsProcessResult<ProcessOutput> {
Expand Down Expand Up @@ -84,9 +83,6 @@ fun GeneralCommandLine.execute(
listener?.let { processHandler.addProcessListener(it) }

val output = try {
if (stdIn != null) {
processHandler.processInput.use { it.write(stdIn) }
}
// execution happens here
processHandler.runner()
} finally {
Expand All @@ -101,7 +97,7 @@ fun GeneralCommandLine.execute(
output.isCancelled -> RsResult.Err(RsProcessExecutionException.Canceled(commandLineString, output))
output.isTimeout -> RsResult.Err(RsProcessExecutionException.Timeout(commandLineString, output))
output.exitCode != 0 -> RsResult.Err(
RsProcessExecutionException.ProcessAborted(
RsProcessExecutionException.FailedWithNonZeroExitCode(
commandLineString,
output
)
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/org/move/openapiext/MvProcessResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ sealed class RsProcessExecutionException : RsProcessExecutionOrDeserializationEx
) : RsProcessExecutionException(errorMessage(commandLineString, output))

/** The process exited with non-zero exit code */
class ProcessAborted(
class FailedWithNonZeroExitCode(
override val commandLineString: String,
val output: ProcessOutput,
) : RsProcessExecutionException(errorMessage(commandLineString, output))
Expand All @@ -65,6 +65,6 @@ fun RsProcessResult<ProcessOutput>.ignoreExitCode(): RsResult<ProcessOutput, RsP
is RsProcessExecutionException.Start -> RsResult.Err(err)
is RsProcessExecutionException.Canceled -> RsResult.Ok(err.output)
is RsProcessExecutionException.Timeout -> RsResult.Ok(err.output)
is RsProcessExecutionException.ProcessAborted -> RsResult.Ok(err.output)
is RsProcessExecutionException.FailedWithNonZeroExitCode -> RsResult.Ok(err.output)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.move.cli.runConfigurations.aptos

import org.move.utils.tests.MvTestBase

class AptosExitStatusTest: MvTestBase() {
fun `test parse result`() {
val status = AptosExitStatus.fromJson(
"""
{
"Result": "my result message"
}
"""
)
check(status is AptosExitStatus.Result)
check(status.message == "my result message")
}

fun `test parse error`() {
val status = AptosExitStatus.fromJson(
"""
{
"Error": "my error message"
}
"""
)
check(status is AptosExitStatus.Error)
check(status.message == "my error message")
}

fun `test parse malformed`() {
val status = AptosExitStatus.fromJson(
"""
{
"Unknown": "unknown"
}
"""
)
check(status is AptosExitStatus.Malformed)
}
}

0 comments on commit 4688ed3

Please sign in to comment.