EG-464 Corda returns incorrect exit code in case if node is started with unknown/missing option (#6010)

* EG-464

Corda returns incorrect exit code in case if node is started with unknown/missing option

* fixed empty else block

* We should not use the parent's exception handler as it will quit and we have our own one

* SampleCordaCliWrapper implemented and tests to verify error handling.

* addressing code review comments
This commit is contained in:
Stefano Franz 2020-03-05 15:38:56 +00:00 committed by GitHub
parent 9a406839fa
commit c3a59f5293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 149 additions and 13 deletions

View File

@ -0,0 +1,50 @@
package net.corda.testing.node.internal
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.matchesPattern
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.stream.Collectors
@RunWith(value = Parameterized::class)
class CordaCliWrapperErrorHandlingTests(val arguments: List<String>, val outputRegexPattern: String) {
companion object {
val className = "net.corda.testing.node.internal.SampleCordaCliWrapper"
private val stackTraceRegex = "^.+Exception[^\\n]++(\\s+at .++)+[\\s\\S]*"
private val exceptionWithoutStackTraceRegex ="${className}(\\s+.+)"
private val emptyStringRegex = "^$"
@JvmStatic
@Parameterized.Parameters
fun data() = listOf(
arrayOf(listOf("--throw-exception", "--verbose"), stackTraceRegex),
arrayOf(listOf("--throw-exception"), exceptionWithoutStackTraceRegex),
arrayOf(listOf("--sample-command"), emptyStringRegex)
)
}
@Test(timeout=300_000)
fun `Run CordaCliWrapper sample app with arguments and check error output matches regExp`() {
val process = ProcessUtilities.startJavaProcess(
className = className,
arguments = arguments,
inheritIO = false)
process.waitFor()
val processErrorOutput = BufferedReader(
InputStreamReader(process.errorStream))
.lines()
.collect(Collectors.joining("\n"))
.toString()
assertThat(processErrorOutput, matchesPattern(outputRegexPattern))
}
}

View File

@ -0,0 +1,36 @@
package net.corda.testing.node.internal
import net.corda.cliutils.ExitCodes
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import kotlin.test.assertEquals
@RunWith(value = Parameterized::class)
class RunCordaNodeReturnCodeTests(val argument: String, val exitCode: Int){
companion object {
@JvmStatic
@Parameterized.Parameters
fun data() = listOf(
arrayOf("--nonExistingOption", ExitCodes.FAILURE),
arrayOf("--help", ExitCodes.SUCCESS),
arrayOf("validate-configuration", ExitCodes.FAILURE),//Should fail as there is no node.conf
arrayOf("initial-registration", ExitCodes.FAILURE) //Missing required option
)
}
@Test(timeout=300_000)
fun runCordaWithArgumentAndAssertExitCode() {
val process = ProcessUtilities.startJavaProcess(
className = "net.corda.node.Corda",
arguments = listOf(argument)
)
process.waitFor()
assertEquals(exitCode, process.exitValue())
}
}

View File

@ -0,0 +1,38 @@
package net.corda.testing.node.internal
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.ExitCodes
import net.corda.cliutils.start
import picocli.CommandLine
class SampleCordaCliWrapperException(message: String) : Exception(message)
class SampleCordaCliWrapper: CordaCliWrapper("sampleCliWrapper", "Sample corda cliWrapper app") {
companion object {
@JvmStatic
fun main(args: Array<String>) {
SampleCordaCliWrapper().start(args)
}
}
@CommandLine.Option(names = ["--sample-command"],
description = [ "Sample command. Prints a message to the console."])
var sampleCommand: Boolean? = null
@CommandLine.Option(names = ["--throw-exception"], description = ["Specify this to throw an exception"])
var throwException: Boolean? = null
override fun runProgram(): Int {
if (throwException!=null) {
throw SampleCordaCliWrapperException("net.corda.testing.node.internal.SampleCordaCliWrapper test exception")
}
if (sampleCommand!=null) {
System.out.println("Sample command invoked.")
}
return ExitCodes.SUCCESS
}
}

View File

@ -37,7 +37,8 @@ object ProcessUtilities {
extraJvmArguments: List<String> = emptyList(), extraJvmArguments: List<String> = emptyList(),
maximumHeapSize: String? = null, maximumHeapSize: String? = null,
identifier: String = "", identifier: String = "",
environmentVariables: Map<String,String> = emptyMap() environmentVariables: Map<String,String> = emptyMap(),
inheritIO: Boolean = true
): Process { ): Process {
val command = mutableListOf<String>().apply { val command = mutableListOf<String>().apply {
add(javaPath) add(javaPath)
@ -49,7 +50,7 @@ object ProcessUtilities {
addAll(arguments) addAll(arguments)
} }
return ProcessBuilder(command).apply { return ProcessBuilder(command).apply {
inheritIO() if (inheritIO) inheritIO()
environment().putAll(environmentVariables) environment().putAll(environmentVariables)
environment()["CLASSPATH"] = classPath.joinToString(File.pathSeparator) environment()["CLASSPATH"] = classPath.joinToString(File.pathSeparator)
if (workingDirectory != null) { if (workingDirectory != null) {

View File

@ -64,15 +64,33 @@ fun CordaCliWrapper.start(args: Array<String>) {
// This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box. // This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box.
AnsiConsole.systemInstall() AnsiConsole.systemInstall()
try {
val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) {
Help.Ansi.ON Help.Ansi.ON
} else { } else {
Help.Ansi.AUTO Help.Ansi.AUTO
} }
val exceptionHandler = object : DefaultExceptionHandler<List<Any>>() {
override fun handleParseException(ex: ParameterException?, args: Array<out String>?): List<Any> {
super.handleParseException(ex, args)
return listOf(ExitCodes.FAILURE)
}
override fun handleExecutionException(ex: ExecutionException, parseResult: ParseResult?): List<Any> {
val throwable = ex.cause ?: ex
if (this@start.verbose || this@start.subCommands.any { it.verbose }) {
throwable.printStackTrace()
}
printError(throwable.rootMessage ?: "Use --verbose for more details")
return listOf(ExitCodes.FAILURE)
}
}
@Suppress("SpreadOperator") @Suppress("SpreadOperator")
val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode),
DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(defaultAnsiMode), *args) exceptionHandler.useErr(System.err).useAnsi(defaultAnsiMode), *args)
// If an error code has been returned, use this and exit // If an error code has been returned, use this and exit
results?.firstOrNull()?.let { results?.firstOrNull()?.let {
if (it is Int) { if (it is Int) {
@ -81,17 +99,10 @@ fun CordaCliWrapper.start(args: Array<String>) {
exitProcess(ExitCodes.FAILURE) exitProcess(ExitCodes.FAILURE)
} }
} }
// If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully // If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully
exitProcess(ExitCodes.SUCCESS) exitProcess(ExitCodes.SUCCESS)
} catch (e: ExecutionException) {
val throwable = e.cause ?: e
if (this.verbose || this.subCommands.any { it.verbose }) {
throwable.printStackTrace()
} else {
}
printError(throwable.rootMessage ?: "Use --verbose for more details")
exitProcess(ExitCodes.FAILURE)
}
} }
@Command(mixinStandardHelpOptions = true, @Command(mixinStandardHelpOptions = true,