diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/CordaCliWrapperErrorHandlingTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/CordaCliWrapperErrorHandlingTests.kt new file mode 100644 index 0000000000..76dad5b90e --- /dev/null +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/CordaCliWrapperErrorHandlingTests.kt @@ -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, 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)) + } +} \ No newline at end of file diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/RunCordaNodeReturnCodeTests.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/RunCordaNodeReturnCodeTests.kt new file mode 100644 index 0000000000..67bc7e880b --- /dev/null +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/RunCordaNodeReturnCodeTests.kt @@ -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()) + } + + +} \ No newline at end of file diff --git a/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/SampleCordaCliWrapper.kt b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/SampleCordaCliWrapper.kt new file mode 100644 index 0000000000..8b91efec2b --- /dev/null +++ b/testing/node-driver/src/integration-test/kotlin/net/corda/testing/node/internal/SampleCordaCliWrapper.kt @@ -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) { + 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 + } + +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt index 9acff0d020..c4a04ecb2a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt @@ -37,7 +37,8 @@ object ProcessUtilities { extraJvmArguments: List = emptyList(), maximumHeapSize: String? = null, identifier: String = "", - environmentVariables: Map = emptyMap() + environmentVariables: Map = emptyMap(), + inheritIO: Boolean = true ): Process { val command = mutableListOf().apply { add(javaPath) @@ -49,7 +50,7 @@ object ProcessUtilities { addAll(arguments) } return ProcessBuilder(command).apply { - inheritIO() + if (inheritIO) inheritIO() environment().putAll(environmentVariables) environment()["CLASSPATH"] = classPath.joinToString(File.pathSeparator) if (workingDirectory != null) { diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index 8bb4130a64..a2ab753251 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -64,15 +64,33 @@ fun CordaCliWrapper.start(args: Array) { // This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box. AnsiConsole.systemInstall() - try { val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { Help.Ansi.ON } else { Help.Ansi.AUTO } + + val exceptionHandler = object : DefaultExceptionHandler>() { + + override fun handleParseException(ex: ParameterException?, args: Array?): List { + super.handleParseException(ex, args) + return listOf(ExitCodes.FAILURE) + } + override fun handleExecutionException(ex: ExecutionException, parseResult: ParseResult?): List { + + 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") val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), - DefaultExceptionHandler>().useErr(System.err).useAnsi(defaultAnsiMode), *args) + exceptionHandler.useErr(System.err).useAnsi(defaultAnsiMode), *args) + + // If an error code has been returned, use this and exit results?.firstOrNull()?.let { if (it is Int) { @@ -81,17 +99,10 @@ fun CordaCliWrapper.start(args: Array) { exitProcess(ExitCodes.FAILURE) } } + // If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully 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,