libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit c5d500902f90df2722359bffba3e7feab42e724f
parent 4495f3d05129fcd5ab0a81b459e8303565365825
Author: Antoine A <>
Date:   Thu, 12 Dec 2024 19:20:40 +0100

testbench: better interactive shell

Diffstat:
MMakefile | 4+++-
Mcommon/src/main/kotlin/Cli.kt | 11+----------
Mtestbench/build.gradle | 2+-
Mtestbench/src/main/kotlin/Main.kt | 140++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
4 files changed, 98 insertions(+), 59 deletions(-)

diff --git a/Makefile b/Makefile @@ -113,7 +113,9 @@ testbench-test: install-nobuild-files .PHONY: testbench testbench: install-nobuild-files - ./gradlew :testbench:run --console=plain --args="$(platform)" + ./gradlew :testbench:install && \ + cd testbench && \ + ./build/install/libeufin-testbench-test/bin/libeufin-testbench-test $(platform) .PHONY: doc doc: diff --git a/common/src/main/kotlin/Cli.kt b/common/src/main/kotlin/Cli.kt @@ -31,8 +31,6 @@ import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.path -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -47,14 +45,7 @@ fun cliCmd(logger: Logger, level: Level, lambda: suspend () -> Unit) { // Run cli command catching all errors try { runBlocking { - val job = launch { - lambda() - } - Runtime.getRuntime().addShutdownHook(object : Thread() { - override fun run() = runBlocking{ - job.cancelAndJoin() - } - }) + lambda() } } catch (e: ProgramResult) { throw e diff --git a/testbench/build.gradle b/testbench/build.gradle @@ -24,7 +24,7 @@ dependencies { implementation("com.github.ajalt.clikt:clikt:$clikt_version") implementation("org.postgresql:postgresql:$postgres_version") - + implementation("org.jline:jline:3.28.0") implementation("io.ktor:ktor-server-test-host:$ktor_version") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -24,20 +24,27 @@ import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.core.ProgramResult import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.parameters.arguments.argument -import com.github.ajalt.clikt.testing.test +import com.github.ajalt.clikt.testing.* import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.http.* -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import kotlinx.serialization.Serializable import tech.libeufin.common.* import tech.libeufin.nexus.* import tech.libeufin.nexus.cli.* import java.time.Instant import kotlin.io.path.* +import org.jline.terminal.* +import org.jline.reader.* +import org.jline.reader.impl.history.* val nexusCmd = LibeufinNexus() val client = HttpClient(CIO) +var thread: Thread? = null +var deferred: CompletableDeferred<CliktCommandTestResult> = CompletableDeferred() + +class Interrupt: Exception("Interrupt") fun step(name: String) { println(ANSI.magenta(name)) @@ -47,21 +54,27 @@ fun msg(msg: String) { println(ANSI.yellow(msg)) } -fun ask(question: String): String? { - print(ANSI.bold(question)) - System.out.flush() - return readlnOrNull() +fun err(msg: String) { + println(ANSI.red(msg)) } -fun CliktCommand.run(arg: String): Boolean { - val res = this.test(arg) +suspend fun CliktCommand.run(arg: String): Boolean { + deferred = CompletableDeferred() + val task = kotlin.concurrent.thread { + deferred.complete(this@run.test(arg)) + } + thread = task + task.join() + thread = null + val res = deferred.await() print(res.output) - if (res.statusCode != 0) { - println(ANSI.red("ERROR ${res.statusCode}")) - } else { + val success = res.statusCode == 0 + if (success) { println(ANSI.green("OK")) + } else { + err("ERROR ${res.statusCode}") } - return res.statusCode == 0 + return success } data class Kind(val name: String, val settings: String?) { @@ -73,6 +86,8 @@ data class Config( val payto: Map<String, String> ) +private val WORDS_REGEX = Regex("\\s+") + class Cli : CliktCommand() { override fun help(context: Context) = "Run integration tests on banks provider" @@ -139,28 +154,50 @@ class Cli : CliktCommand() { "Missing dummy payto for $currency" } val payto = benchCfg.payto[currency] ?: dummyPayto - val recoverDoc = "report statement notification" + + // Prepare shell + val terminal = TerminalBuilder.builder().system(true).build()//.signalHandler(Terminal.SignalHandler.SIG_IGN).build() + val history = DefaultHistory() + val reader = LineReaderBuilder.builder().terminal(terminal).history(history).build(); + + terminal.handle(Terminal.Signal.INT) { + thread?.let { + thread = null + it.interrupt() + } ?: run { + kotlin.system.exitProcess(0) + } + } + runBlocking { step("Init ${kind.name}") assert(nexusCmd.run("dbinit $flags")) val cmds = buildMap { - fun put(name: String, args: String) { - put(name, suspend { - nexusCmd.run(args) + fun putCmd(name: String, step: String, lambda: suspend (List<String>) -> Unit) { + put(name, Pair(step, lambda)) + } + fun put(name: String, step: String, lambda: suspend () -> Unit) { + putCmd(name, step, { + lambda() Unit }) } fun put(name: String, step: String, args: String) { - put(name, suspend { - step(step) + put(name, step, { nexusCmd.run(args) Unit }) } - put("reset-db", "dbinit -r $flags") + fun putArgs(name: String, step: String, parser: (List<String>) -> String) { + putCmd(name, step, { args: List<String> -> + nexusCmd.run(parser(args)) + Unit + }) + } + put("reset-db", "Reset DB", "dbinit -r $flags") put("recover", "Recover old transactions", "ebics-fetch $ebicsFlags --pinned-start 2024-01-01 $recoverDoc") put("fetch", "Fetch all documents", "ebics-fetch $ebicsFlags") put("fetch-wait", "Fetch all documents", "ebics-fetch $debugFlags") @@ -178,48 +215,46 @@ class Cli : CliktCommand() { put("submit", "Submit pending transactions", "ebics-submit $ebicsFlags") put("export", "Export pending batches as pain001 messages", "manual export $flags payments.zip") put("setup", "Setup", "ebics-setup $debugFlags") - put("reset-keys", suspend { + putArgs("status", "Set batch or transaction status") { + "manual status $flags " + it.joinToString(" ") + } + put("reset-keys", "Reset EBICS keys") { if (kind.test) { clientKeysPath.deleteIfExists() } bankKeysPath.deleteIfExists() Unit - }) - put("tx", suspend { - step("Initiate a new transaction") + } + put("tx", "Initiate a new transaction") { val now = Instant.now() nexusCmd.run("initiate-payment $flags --amount=$currency:0.1 --subject \"single $now\" \"$payto\"") Unit - }) - put("txs", suspend { - step("Initiate four new transactions") + } + put("txs", "Initiate four new transactions") { val now = Instant.now() repeat(4) { nexusCmd.run("initiate-payment $flags --amount=$currency:${(10.0+it)/100} --subject \"multi $it $now\" \"$payto\"") } - }) - put("tx-bad-name", suspend { + } + put("tx-bad-name", "Initiate a new transaction with a bad name") { val badPayto = URLBuilder().takeFrom(payto) badPayto.parameters["receiver-name"] = "John Smith" - step("Initiate a new transaction with a bad name") val now = Instant.now() nexusCmd.run("initiate-payment $flags --amount=$currency:0.21 --subject \"bad name $now\" \"$badPayto\"") Unit - }) - put("tx-bad-iban", suspend { + } + put("tx-bad-iban", "Initiate a new transaction to a bad IBAN") { val badPayto = URLBuilder().takeFrom("payto://iban/XX18500105173385245165") badPayto.parameters["receiver-name"] = "John Smith" - step("Initiate a new transaction to a bad IBAN") val now = Instant.now() nexusCmd.run("initiate-payment $flags --amount=$currency:0.22 --subject \"bad iban $now\" \"$badPayto\"") Unit - }) - put("tx-dummy-iban", suspend { - step("Initiate a new transaction to a dummy IBAN") + } + put("tx-dummy-iban", "Initiate a new transaction to a dummy IBAN") { val now = Instant.now() nexusCmd.run("initiate-payment $flags --amount=$currency:0.23 --subject \"dummy iban $now\" \"$dummyPayto\"") Unit - }) + } put("tx-check", "Check transaction semantic", "testing tx-check $flags") } while (true) { @@ -246,19 +281,30 @@ class Cli : CliktCommand() { } } // REPL - val arg = ask("testbench> ")!!.trim() - if (arg == "exit") break - if (arg == "") continue - val cmd = cmds[arg] + val line = try { + reader.readLine("testbench> ")!! + } catch (e: UserInterruptException) { + print(ANSI.red("^C")) + System.out.flush() + throw ProgramResult(1) + } + val args = line.split(WORDS_REGEX).toMutableList() + val cmdArg = args.removeFirstOrNull() + val cmd = cmds[cmdArg] if (cmd != null) { - cmd() + step(cmd.first) + cmd.second(args) } else { - if (arg != "?" && arg != "help") { - println("Unknown command '$arg'") - } - println("Commands:") - for ((name, _) in cmds) { - println(" $name") + when (cmdArg) { + "" -> continue + "exit" -> break + "?", "help" -> { + println("Commands:") + for ((name, cmd) in cmds) { + println(" $name - ${cmd.first}") + } + } + else -> err("Unknown command '$cmdArg'") } } }