Improve the error messages printed by the shell when a flow c'tor doesn't match.

This commit is contained in:
Mike Hearn 2018-08-21 23:27:24 +02:00
parent 63ebc394bf
commit 2d39b39e31
3 changed files with 83 additions and 10 deletions

View File

@ -47,8 +47,7 @@ import java.io.FileDescriptor
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.io.PrintWriter import java.io.PrintWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.*
import java.lang.reflect.UndeclaredThrowableException
import java.nio.file.Path import java.nio.file.Path
import java.util.* import java.util.*
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@ -300,6 +299,38 @@ object InteractiveShell {
override fun toString() = (listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator()) override fun toString() = (listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator())
} }
/**
* Tidies up a possibly generic type name by chopping off the package names of classes in a hard-coded set of
* hierarchies that are known to be widely used and recognised, and also not have (m)any ambiguous names in them.
*
* This is used for printing error messages when something doesn't match.
*/
private fun maybeAbbreviateGenericType(type: Type, extraRecognisedPackage: String): String {
val packagesToAbbreviate = listOf("java.", "net.corda.core.", "kotlin.", extraRecognisedPackage)
fun shouldAbbreviate(typeName: String) = packagesToAbbreviate.any { typeName.startsWith(it) }
fun abbreviated(typeName: String) = if (shouldAbbreviate(typeName)) typeName.split('.').last() else typeName
fun innerLoop(type: Type): String = when (type) {
is ParameterizedType -> {
val args: List<String> = type.actualTypeArguments.map(::innerLoop)
abbreviated(type.rawType.typeName) + '<' + args.joinToString(", ") + '>'
}
is GenericArrayType -> {
innerLoop(type.genericComponentType) + "[]"
}
is Class<*> -> {
if (type.isArray)
abbreviated(type.simpleName)
else
abbreviated(type.name).replace('$', '.')
}
else -> type.toString()
}
return innerLoop(type)
}
// TODO: This utility is generally useful and might be better moved to the node class, or an RPC, if we can commit to making it stable API. // TODO: This utility is generally useful and might be better moved to the node class, or an RPC, if we can commit to making it stable API.
/** /**
* Given a [FlowLogic] class and a string in one-line Yaml form, finds an applicable constructor and starts * Given a [FlowLogic] class and a string in one-line Yaml form, finds an applicable constructor and starts
@ -319,10 +350,17 @@ object InteractiveShell {
// and keep track of the reasons we failed so we can print them out if no constructors are usable. // and keep track of the reasons we failed so we can print them out if no constructors are usable.
val parser = StringToMethodCallParser(clazz, om) val parser = StringToMethodCallParser(clazz, om)
val errors = ArrayList<String>() val errors = ArrayList<String>()
val classPackage = clazz.packageName
for (ctor in clazz.constructors) { for (ctor in clazz.constructors) {
var paramNamesFromConstructor: List<String>? = null var paramNamesFromConstructor: List<String>? = null
fun getPrototype(): List<String> { fun getPrototype(): List<String> {
val argTypes = ctor.genericParameterTypes.map { it.typeName } val argTypes = ctor.genericParameterTypes.map { it: Type ->
// If the type name is in the net.corda.core or java namespaces, chop off the package name
// because these hierarchies don't have (m)any ambiguous names and the extra detail is just noise.
maybeAbbreviateGenericType(it, classPackage)
}
return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" } return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" }
} }

View File

@ -52,8 +52,8 @@ public class InteractiveShellJavaTest {
} }
} }
public FlowA(Integer b) { public FlowA(int b) {
this(b.toString()); this(Integer.valueOf(b).toString());
} }
public FlowA(Integer b, String c) { public FlowA(Integer b, String c) {
@ -111,6 +111,9 @@ public class InteractiveShellJavaTest {
this.a = a; this.a = a;
} }
public FlowB(Amount<Currency> amount, int abc) {
}
@Nullable @Nullable
@Override @Override
public ProgressTracker getProgressTracker() { public ProgressTracker getProgressTracker() {
@ -142,6 +145,7 @@ public class InteractiveShellJavaTest {
this.label = label; this.label = label;
} }
@SuppressWarnings("unused") // Used via reflection.
public String getLabel() { public String getLabel() {
return label; return label;
} }
@ -160,17 +164,17 @@ public class InteractiveShellJavaTest {
private void check(String input, String expected, Class<? extends StringFlow> flowClass) throws InteractiveShell.NoApplicableConstructor { private void check(String input, String expected, Class<? extends StringFlow> flowClass) throws InteractiveShell.NoApplicableConstructor {
InteractiveShell.INSTANCE.runFlowFromString((clazz, args) -> { InteractiveShell.INSTANCE.runFlowFromString((clazz, args) -> {
StringFlow instance = null; StringFlow instance = null;
try { try {
instance = (StringFlow)clazz.getConstructor(Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)).newInstance(args); instance = (StringFlow)clazz.getConstructor(Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)).newInstance(args);
} catch (Exception e) { } catch (Exception e) {
System.out.println(e); System.out.println(e);
throw new RuntimeException(e);
} }
output = instance.getA(); output = instance.getA();
OpenFuture<String> future = CordaFutureImplKt.openFuture(); OpenFuture<String> future = CordaFutureImplKt.openFuture();
future.set("ABC"); future.set("ABC");
return new FlowProgressHandleImpl(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string")); return new FlowProgressHandleImpl<String>(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string"));
}, input, flowClass, om); }, input, flowClass, om);
assertEquals(input, expected, output); assertEquals(input, expected, output);
} }
@ -245,4 +249,14 @@ public class InteractiveShellJavaTest {
public void unwrapLambda() throws InteractiveShell.NoApplicableConstructor { public void unwrapLambda() throws InteractiveShell.NoApplicableConstructor {
check("party: \"" + megaCorp.getName() + "\", a: Bambam", "Bambam", FlowB.class); check("party: \"" + megaCorp.getName() + "\", a: Bambam", "Bambam", FlowB.class);
} }
@Test
public void niceErrors() {
// Most cases are checked in the Kotlin test, so we only check raw types here.
try {
check("amount: $100", "", FlowB.class);
} catch (InteractiveShell.NoApplicableConstructor e) {
assertEquals("[amount: Amount<Currency>, abc: int]: missing parameter abc", e.getErrors().get(1));
}
}
} }

View File

@ -14,12 +14,13 @@ import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.FlowProgressHandleImpl import net.corda.core.messaging.FlowProgressHandleImpl
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.testing.internal.DEV_ROOT_CA
import net.corda.testing.core.TestIdentity import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.DEV_ROOT_CA
import org.junit.Test import org.junit.Test
import rx.Observable import rx.Observable
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class InteractiveShellTest { class InteractiveShellTest {
companion object { companion object {
@ -28,7 +29,7 @@ class InteractiveShellTest {
@Suppress("UNUSED") @Suppress("UNUSED")
class FlowA(val a: String) : FlowLogic<String>() { class FlowA(val a: String) : FlowLogic<String>() {
constructor(b: Int?) : this(b.toString()) constructor(b: Int) : this(b.toString())
constructor(b: Int?, c: String) : this(b.toString() + c) constructor(b: Int?, c: String) : this(b.toString() + c)
constructor(amount: Amount<Currency>) : this(amount.toString()) constructor(amount: Amount<Currency>) : this(amount.toString())
constructor(pair: Pair<Amount<Currency>, SecureHash.SHA256>) : this(pair.toString()) constructor(pair: Pair<Amount<Currency>, SecureHash.SHA256>) : this(pair.toString())
@ -48,7 +49,6 @@ class InteractiveShellTest {
private fun check(input: String, expected: String) { private fun check(input: String, expected: String) {
var output: String? = null var output: String? = null
InteractiveShell.runFlowFromString({ clazz, args -> InteractiveShell.runFlowFromString({ clazz, args ->
val instance = clazz.getConstructor(*args.map { it!!::class.java }.toTypedArray()).newInstance(*args) as FlowA val instance = clazz.getConstructor(*args.map { it!!::class.java }.toTypedArray()).newInstance(*args) as FlowA
output = instance.a output = instance.a
val future = openFuture<String>() val future = openFuture<String>()
@ -101,6 +101,27 @@ class InteractiveShellTest {
@Test(expected = InteractiveShell.NoApplicableConstructor::class) @Test(expected = InteractiveShell.NoApplicableConstructor::class)
fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "") fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "")
@Test
fun niceTypeNamesInErrors() {
val e = assertFailsWith<InteractiveShell.NoApplicableConstructor> {
check("", expected = "")
}
val correct = setOf(
"[amounts: Amount<InteractiveShellTest.UserValue>[]]: missing parameter amounts",
"[amount: Amount<Currency>]: missing parameter amount",
"[pair: Pair<Amount<Currency>, SecureHash.SHA256>]: missing parameter pair",
"[party: Party]: missing parameter party",
"[b: Integer, amount: Amount<InteractiveShellTest.UserValue>]: missing parameter b",
"[b: String[]]: missing parameter b",
"[b: Integer, c: String]: missing parameter b",
"[a: String]: missing parameter a",
"[b: int]: missing parameter b"
)
val errors = e.errors.toHashSet()
errors.removeAll(correct)
assert(errors.isEmpty()) { errors.joinToString(", ") }
}
@Test @Test
fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString()) fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString())