commit c5d500902f90df2722359bffba3e7feab42e724f
parent 4495f3d05129fcd5ab0a81b459e8303565365825
Author: Antoine A <>
Date: Thu, 12 Dec 2024 19:20:40 +0100
testbench: better interactive shell
Diffstat:
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'")
}
}
}