diff --git a/scripts/cn1playground/common/src/main/java/bsh/cn1/CN1LambdaSupport.java b/scripts/cn1playground/common/src/main/java/bsh/cn1/CN1LambdaSupport.java index 68b9bfc76b..9392cc9975 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/cn1/CN1LambdaSupport.java +++ b/scripts/cn1playground/common/src/main/java/bsh/cn1/CN1LambdaSupport.java @@ -9,6 +9,15 @@ public final class CN1LambdaSupport { private static final ThreadLocal CURRENT_INTERPRETER = new ThreadLocal(); private static final ThreadLocal CURRENT_NAMESPACE = new ThreadLocal(); + private static final ThreadLocal CURRENT_ERROR_HANDLER = new ThreadLocal(); + + /** Callback invoked when a lambda body raises an exception during + * {@link LambdaValue#invoke(Object[])}. Lets a host (e.g. the + * playground UI) surface runtime failures that would otherwise be + * lost to the EDT's silent exception handling. */ + public interface LambdaErrorHandler { + void onLambdaError(Throwable error, String bodySource); + } private CN1LambdaSupport() { } @@ -33,6 +42,19 @@ public static void clearCurrentNameSpace() { CURRENT_NAMESPACE.remove(); } + /** Register a {@link LambdaErrorHandler} that will be captured by any + * {@link LambdaValue} created on this thread while the handler is + * active. The captured handler stays with the lambda for its + * lifetime so errors raised on later EDT firings can still surface + * back to the host. */ + public static void pushErrorHandler(LambdaErrorHandler handler) { + CURRENT_ERROR_HANDLER.set(handler); + } + + public static void clearErrorHandler() { + CURRENT_ERROR_HANDLER.remove(); + } + public static LambdaValue lambda(String[] parameterNames, String bodySource) { Interpreter interpreter = CURRENT_INTERPRETER.get(); if (interpreter == null) { @@ -40,7 +62,8 @@ public static LambdaValue lambda(String[] parameterNames, String bodySource) { } NameSpace activeNs = CURRENT_NAMESPACE.get(); NameSpace parentNs = activeNs != null ? activeNs : interpreter.getNameSpace(); - return new LambdaValue(interpreter, parentNs, sanitizeParams(parameterNames), bodySource == null ? "" : bodySource); + return new LambdaValue(interpreter, parentNs, sanitizeParams(parameterNames), + bodySource == null ? "" : bodySource, CURRENT_ERROR_HANDLER.get()); } private static String[] sanitizeParams(String[] parameterNames) { @@ -135,12 +158,15 @@ public static final class LambdaValue implements private final NameSpace parentNameSpace; private final String[] parameterNames; private final String bodySource; + private final LambdaErrorHandler errorHandler; - LambdaValue(Interpreter interpreter, NameSpace parentNameSpace, String[] parameterNames, String bodySource) { + LambdaValue(Interpreter interpreter, NameSpace parentNameSpace, String[] parameterNames, + String bodySource, LambdaErrorHandler errorHandler) { this.interpreter = interpreter; this.parentNameSpace = parentNameSpace; this.parameterNames = parameterNames; this.bodySource = bodySource; + this.errorHandler = errorHandler; } /** Runnable adapter — zero-arg lambda. */ @@ -206,7 +232,9 @@ public Object invoke(Object[] args) throws EvalError { lambdaNs.setVariable(parameterNames[i], safeArgs[i], false); } } catch (UtilEvalError ex) { - throw ex.toEvalError(null, null); + EvalError converted = ex.toEvalError(null, null); + reportError(converted); + throw converted; } Interpreter prevInterpreter = CURRENT_INTERPRETER.get(); NameSpace prevNamespace = CURRENT_NAMESPACE.get(); @@ -216,6 +244,12 @@ public Object invoke(Object[] args) throws EvalError { synchronized (interpreter) { return Primitive.unwrap(interpreter.eval(bodySource, lambdaNs)); } + } catch (EvalError ex) { + reportError(ex); + throw ex; + } catch (RuntimeException ex) { + reportError(ex); + throw ex; } finally { if (prevInterpreter != null) { CURRENT_INTERPRETER.set(prevInterpreter); @@ -229,6 +263,17 @@ public Object invoke(Object[] args) throws EvalError { } } } + + private void reportError(Throwable error) { + if (errorHandler == null) { + return; + } + try { + errorHandler.onLambdaError(error, bodySource); + } catch (Throwable ignored) { + // Reporter failures must not displace the real lambda error. + } + } } /** Adapt a {@link LambdaValue} to a {@link java.util.function.BinaryOperator} diff --git a/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java b/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java index d9cae002f4..670a383acc 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java +++ b/scripts/cn1playground/common/src/main/java/bsh/cn1/GeneratedCN1Access.java @@ -726,6 +726,7 @@ public final class GeneratedCN1Access implements CN1Access { "com.codenameone.playground.CN1Playground", "com.codenameone.playground.PlaygroundContext", "com.codenameone.playground.PlaygroundContext.Logger", + "com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter", "com.codenameone.playground.PlaygroundLambdaBridge", "com.codenameone.playground.PlaygroundListenerBridge", "com.codenameone.playground.WebsiteThemeNative", @@ -1599,8 +1600,9 @@ private static void fillMethodIndex9(Map index) { index.put("com.codename1.xml.XMLParser", splitMembers("")); index.put("com.codename1.xml.XMLWriter", splitMembers("")); index.put("com.codenameone.playground.CN1Playground", splitMembers("destroy()getTheme()init(Object)runApp()start()stop()")); - index.put("com.codenameone.playground.PlaygroundContext", splitMembers("captureShownForm(Form)clearCreatedComponents()clearPreview()clearShownForm()getCreatedComponents()getFirstCreatedComponent()getFirstCreatedForm()getHostForm()getPreviewRoot()getShownForm()getTheme()log(String)recordCreatedComponent(Component)refreshPreview()setTitle(String)debug(String)getCurrent()interceptMethodInvocation(Object, String, Object[])notifyConstructed(Object)")); + index.put("com.codenameone.playground.PlaygroundContext", splitMembers("captureShownForm(Form)clearCreatedComponents()clearPreview()clearShownForm()getCreatedComponents()getFirstCreatedComponent()getFirstCreatedForm()getHostForm()getPreviewRoot()getShownForm()getTheme()log(String)recordCreatedComponent(Component)refreshPreview()reportRuntimeError(String, Throwable)setTitle(String)debug(String)getCurrent()interceptMethodInvocation(Object, String, Object[])notifyConstructed(Object)")); index.put("com.codenameone.playground.PlaygroundContext.Logger", splitMembers("log(String)")); + index.put("com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter", splitMembers("reportRuntimeError(String, Throwable)")); index.put("com.codenameone.playground.PlaygroundLambdaBridge", splitMembers("lambda(Object[], String)lambda(String[], String)")); index.put("com.codenameone.playground.PlaygroundListenerBridge", splitMembers("actionListener(Object)networkListener(Object)onComplete(Object)runnable(Object)")); index.put("com.codenameone.playground.WebsiteThemeNative", splitMembers("isDarkMode()isSupported()notifyUiReady()")); @@ -1608,10 +1610,10 @@ private static void fillMethodIndex9(Map index) { index.put("java.io.ByteArrayOutputStream", splitMembers("")); index.put("java.io.DataInput", splitMembers("")); index.put("java.io.DataInputStream", splitMembers("")); - index.put("java.io.DataOutput", splitMembers("")); } private static void fillMethodIndex10(Map index) { + index.put("java.io.DataOutput", splitMembers("")); index.put("java.io.DataOutputStream", splitMembers("")); index.put("java.io.EOFException", splitMembers("")); index.put("java.io.Flushable", splitMembers("")); @@ -1675,10 +1677,10 @@ private static void fillMethodIndex10(Map index) { index.put("java.lang.OutOfMemoryError", splitMembers("")); index.put("java.lang.Override", splitMembers("")); index.put("java.lang.Runnable", splitMembers("")); - index.put("java.lang.Runtime", splitMembers("")); } private static void fillMethodIndex11(Map index) { + index.put("java.lang.Runtime", splitMembers("")); index.put("java.lang.RuntimeException", splitMembers("")); index.put("java.lang.SafeVarargs", splitMembers("")); index.put("java.lang.SecurityException", splitMembers("")); @@ -1742,10 +1744,10 @@ private static void fillMethodIndex11(Map index) { index.put("java.util.Comparator", splitMembers("")); index.put("java.util.ConcurrentModificationException", splitMembers("")); index.put("java.util.Date", splitMembers("")); - index.put("java.util.Deque", splitMembers("")); } private static void fillMethodIndex12(Map index) { + index.put("java.util.Deque", splitMembers("")); index.put("java.util.Dictionary", splitMembers("")); index.put("java.util.EmptyStackException", splitMembers("")); index.put("java.util.Enumeration", splitMembers("")); @@ -2476,6 +2478,7 @@ private static void fillFieldIndex9(Map index) { index.put("com.codenameone.playground.CN1Playground", splitMembers("")); index.put("com.codenameone.playground.PlaygroundContext", splitMembers("")); index.put("com.codenameone.playground.PlaygroundContext.Logger", splitMembers("")); + index.put("com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter", splitMembers("")); index.put("com.codenameone.playground.PlaygroundLambdaBridge", splitMembers("")); index.put("com.codenameone.playground.PlaygroundListenerBridge", splitMembers("")); index.put("com.codenameone.playground.WebsiteThemeNative", splitMembers("")); @@ -2483,10 +2486,10 @@ private static void fillFieldIndex9(Map index) { index.put("java.io.ByteArrayOutputStream", splitMembers("")); index.put("java.io.DataInput", splitMembers("")); index.put("java.io.DataInputStream", splitMembers("")); - index.put("java.io.DataOutput", splitMembers("")); } private static void fillFieldIndex10(Map index) { + index.put("java.io.DataOutput", splitMembers("")); index.put("java.io.DataOutputStream", splitMembers("")); index.put("java.io.EOFException", splitMembers("")); index.put("java.io.Flushable", splitMembers("")); @@ -2550,10 +2553,10 @@ private static void fillFieldIndex10(Map index) { index.put("java.lang.OutOfMemoryError", splitMembers("")); index.put("java.lang.Override", splitMembers("")); index.put("java.lang.Runnable", splitMembers("")); - index.put("java.lang.Runtime", splitMembers("")); } private static void fillFieldIndex11(Map index) { + index.put("java.lang.Runtime", splitMembers("")); index.put("java.lang.RuntimeException", splitMembers("")); index.put("java.lang.SafeVarargs", splitMembers("")); index.put("java.lang.SecurityException", splitMembers("")); @@ -2617,10 +2620,10 @@ private static void fillFieldIndex11(Map index) { index.put("java.util.Comparator", splitMembers("")); index.put("java.util.ConcurrentModificationException", splitMembers("")); index.put("java.util.Date", splitMembers("")); - index.put("java.util.Deque", splitMembers("")); } private static void fillFieldIndex12(Map index) { + index.put("java.util.Deque", splitMembers("")); index.put("java.util.Dictionary", splitMembers("")); index.put("java.util.EmptyStackException", splitMembers("")); index.put("java.util.Enumeration", splitMembers("")); diff --git a/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java b/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java index 534b28f9a1..e0aa52581c 100644 --- a/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java +++ b/scripts/cn1playground/common/src/main/java/bsh/cn1/gen/GeneratedAccess_com_codenameone_playground.java @@ -33,6 +33,9 @@ private static Class findClassChunk0(String simpleName) { if ("Logger".equals(simpleName)) { return com.codenameone.playground.PlaygroundContext.Logger.class; } + if ("RuntimeErrorReporter".equals(simpleName)) { + return com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter.class; + } if ("PlaygroundLambdaBridge".equals(simpleName)) { return com.codenameone.playground.PlaygroundLambdaBridge.class; } @@ -51,6 +54,10 @@ public static Object construct(Class type, Object[] args) throws Exception { Object[] adaptedArgs = adaptArgs(safeArgs, new Class[]{com.codename1.ui.Form.class, com.codename1.ui.Container.class, com.codename1.ui.util.Resources.class, com.codenameone.playground.PlaygroundContext.Logger.class}, false); return new com.codenameone.playground.PlaygroundContext((com.codename1.ui.Form) adaptedArgs[0], (com.codename1.ui.Container) adaptedArgs[1], (com.codename1.ui.util.Resources) adaptedArgs[2], (com.codenameone.playground.PlaygroundContext.Logger) adaptedArgs[3]); } + if (matches(safeArgs, new Class[]{com.codename1.ui.Form.class, com.codename1.ui.Container.class, com.codename1.ui.util.Resources.class, com.codenameone.playground.PlaygroundContext.Logger.class, com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter.class}, false)) { + Object[] adaptedArgs = adaptArgs(safeArgs, new Class[]{com.codename1.ui.Form.class, com.codename1.ui.Container.class, com.codename1.ui.util.Resources.class, com.codenameone.playground.PlaygroundContext.Logger.class, com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter.class}, false); + return new com.codenameone.playground.PlaygroundContext((com.codename1.ui.Form) adaptedArgs[0], (com.codename1.ui.Container) adaptedArgs[1], (com.codename1.ui.util.Resources) adaptedArgs[2], (com.codenameone.playground.PlaygroundContext.Logger) adaptedArgs[3], (com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter) adaptedArgs[4]); + } } throw unsupportedConstruct(type, safeArgs); } @@ -126,9 +133,16 @@ public static Object invoke(Object target, String name, Object[] args) throws Ex unsupported = ex; } } + if (target instanceof com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter) { + try { + return invoke5((com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter) target, name, safeArgs); + } catch (CN1AccessException ex) { + unsupported = ex; + } + } if (target instanceof com.codenameone.playground.WebsiteThemeNative) { try { - return invoke5((com.codenameone.playground.WebsiteThemeNative) target, name, safeArgs); + return invoke6((com.codenameone.playground.WebsiteThemeNative) target, name, safeArgs); } catch (CN1AccessException ex) { unsupported = ex; } @@ -248,6 +262,12 @@ private static Object invoke1(com.codenameone.playground.PlaygroundContext typed typedTarget.refreshPreview(); return null; } } + if ("reportRuntimeError".equals(name)) { + if (matches(safeArgs, new Class[]{java.lang.String.class, java.lang.Throwable.class}, false)) { + Object[] adaptedArgs = adaptArgs(safeArgs, new Class[]{java.lang.String.class, java.lang.Throwable.class}, false); + typedTarget.reportRuntimeError((java.lang.String) adaptedArgs[0], (java.lang.Throwable) adaptedArgs[1]); return null; + } + } if ("setTitle".equals(name)) { if (matches(safeArgs, new Class[]{java.lang.String.class}, false)) { Object[] adaptedArgs = adaptArgs(safeArgs, new Class[]{java.lang.String.class}, false); @@ -309,7 +329,17 @@ private static Object invoke4(com.codenameone.playground.PlaygroundContext.Logge throw unsupportedInstance(typedTarget, name, safeArgs); } - private static Object invoke5(com.codenameone.playground.WebsiteThemeNative typedTarget, String name, Object[] safeArgs) throws Exception { + private static Object invoke5(com.codenameone.playground.PlaygroundContext.RuntimeErrorReporter typedTarget, String name, Object[] safeArgs) throws Exception { + if ("reportRuntimeError".equals(name)) { + if (matches(safeArgs, new Class[]{java.lang.String.class, java.lang.Throwable.class}, false)) { + Object[] adaptedArgs = adaptArgs(safeArgs, new Class[]{java.lang.String.class, java.lang.Throwable.class}, false); + typedTarget.reportRuntimeError((java.lang.String) adaptedArgs[0], (java.lang.Throwable) adaptedArgs[1]); return null; + } + } + throw unsupportedInstance(typedTarget, name, safeArgs); + } + + private static Object invoke6(com.codenameone.playground.WebsiteThemeNative typedTarget, String name, Object[] safeArgs) throws Exception { if ("isDarkMode".equals(name)) { if (safeArgs.length == 0) { return typedTarget.isDarkMode(); diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/CN1Playground.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/CN1Playground.java index b84026413d..ca5c2e358e 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/CN1Playground.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/CN1Playground.java @@ -81,6 +81,7 @@ public class CN1Playground extends Lifecycle { private String currentMobileTab = MOBILE_TAB_CODE; private List currentMessages = new ArrayList<>(); private List currentCssMessages = new ArrayList<>(); + private final List currentRuntimeErrors = new ArrayList<>(); private int editSequence; private int autoRunSequence; private int historySequence; @@ -716,11 +717,17 @@ private void runScript(Form form) { private void executeRunScript(Form form) { CN.callSerially(() -> { List loggedMessages = new ArrayList<>(); + // Lambdas wired up by the previous run may still be holding + // references to widgets we just replaced; clear their stale + // error trail before this run so the panel doesn't show + // failures from a script that is no longer on screen. + currentRuntimeErrors.clear(); PlaygroundContext context = new PlaygroundContext( form, previewColumn.getContentHost(), theme, - message -> loggedMessages.add(new PlaygroundRunner.InlineMessage(0, message, "info")) + message -> loggedMessages.add(new PlaygroundRunner.InlineMessage(0, message, "info")), + this::reportLambdaRuntimeError ); PlaygroundRunner.RunResult result = runner.run(currentScript, context); @@ -754,6 +761,40 @@ private void executeRunScript(Form form) { }); } + /** Receives runtime errors raised by lambdas after a script's initial + * eval — the most common cause is a missing import (so an identifier + * like {@code Util} is unresolved when the event listener fires). The + * EDT would otherwise swallow these silently, leaving the user with + * a UI that no longer reacts. We surface each unique error as an + * inline editor message and flip the top bar to its failed state. */ + private void reportLambdaRuntimeError(String message, Throwable cause) { + if (message == null || message.isEmpty()) { + return; + } + CN.callSerially(() -> { + // Dedup so a listener that keeps firing (e.g. text-field + // keystrokes) doesn't append the same message dozens of times. + for (int i = 0; i < currentRuntimeErrors.size(); i++) { + if (message.equals(currentRuntimeErrors.get(i).text)) { + return; + } + } + PlaygroundRunner.InlineMessage entry = + new PlaygroundRunner.InlineMessage(0, "Runtime error: " + message, "error"); + currentRuntimeErrors.add(entry); + currentMessages.add(entry); + if (editor != null) { + editor.setInlineMessages(currentMessages); + } + if (topBar != null) { + topBar.showFailed(); + } + if (previewColumn != null) { + previewColumn.setStale(true); + } + }); + } + private void replacePreview(Component component) { if (component == null) { previewColumn.setPreview(null); diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java index 6245c882ba..ae943ad48b 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundContext.java @@ -20,20 +20,35 @@ public interface Logger { void log(String message); } + /** Receives runtime errors that happen after a script has finished its + * initial evaluation — typically a lambda body that fails on a later + * event firing. Without this hook the EDT silently swallows the + * exception and the user sees a UI that no longer reacts to input. */ + public interface RuntimeErrorReporter { + void reportRuntimeError(String message, Throwable cause); + } + private final Form hostForm; private final Container previewRoot; private final Resources theme; private final Logger logger; + private final RuntimeErrorReporter runtimeErrorReporter; private Form shownForm; private final List createdComponents = new ArrayList(); private Form firstCreatedForm; private Component firstCreatedComponent; public PlaygroundContext(Form hostForm, Container previewRoot, Resources theme, Logger logger) { + this(hostForm, previewRoot, theme, logger, null); + } + + public PlaygroundContext(Form hostForm, Container previewRoot, Resources theme, Logger logger, + RuntimeErrorReporter runtimeErrorReporter) { this.hostForm = hostForm; this.previewRoot = previewRoot; this.theme = theme; this.logger = logger; + this.runtimeErrorReporter = runtimeErrorReporter; } public Form getHostForm() { @@ -97,6 +112,12 @@ public void log(String message) { logger.log(message); } + public void reportRuntimeError(String message, Throwable cause) { + if (runtimeErrorReporter != null) { + runtimeErrorReporter.reportRuntimeError(message, cause); + } + } + public void captureShownForm(Form form) { shownForm = form; } diff --git a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java index 04f70299ee..3ca75d7a3f 100644 --- a/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java +++ b/scripts/cn1playground/common/src/main/java/com/codenameone/playground/PlaygroundRunner.java @@ -81,6 +81,26 @@ List getMessages() { } } + /** Bridges a lambda runtime failure into the playground's + * {@link PlaygroundContext#reportRuntimeError(String, Throwable)} + * so the editor can surface the error instead of having the EDT + * silently swallow it. */ + private final class LambdaErrorBridge implements CN1LambdaSupport.LambdaErrorHandler { + private final PlaygroundContext context; + + LambdaErrorBridge(PlaygroundContext context) { + this.context = context; + } + + @Override + public void onLambdaError(Throwable error, String bodySource) { + if (context == null || error == null) { + return; + } + context.reportRuntimeError(safeMessage(error), error); + } + } + RunResult run(String script, PlaygroundContext context) { List inlineMessages = new ArrayList(); try { @@ -92,6 +112,11 @@ RunResult run(String script, PlaygroundContext context) { } PlaygroundContext.pushCurrent(context); CN1LambdaSupport.pushInterpreter(interpreter); + // Lambdas created during eval capture this handler and use it + // to surface their runtime failures back to the host. We push + // it even when the context can't actually report (no reporter + // wired) — the handler short-circuits in that case. + CN1LambdaSupport.pushErrorHandler(new LambdaErrorBridge(context)); try { ScriptPlan plan = adaptScript(script); for (int i = 0; i < plan.typeDeclarations.size(); i++) { @@ -102,6 +127,7 @@ RunResult run(String script, PlaygroundContext context) { inlineMessages.add(new InlineMessage(0, "Preview updated.", "success")); return new RunResult(component, Collections.emptyList(), inlineMessages); } finally { + CN1LambdaSupport.clearErrorHandler(); CN1LambdaSupport.clearInterpreter(); PlaygroundContext.clearCurrent(); } diff --git a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java index de1a6b2927..57f9229d87 100644 --- a/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java +++ b/scripts/cn1playground/common/src/test/java/com/codenameone/playground/PlaygroundSmokeHarness.java @@ -26,6 +26,7 @@ public static void main(String[] args) throws Exception { smokeComponentTypeResolvesWithoutExplicitImport(); smokeUIManagerClassImportDoesNotCollideWithGlobals(); smokeInstanceDispatchSuggestsStaticUtility(); + smokeLambdaRuntimeErrorSurfacesToReporter(); System.out.println("Playground smoke tests passed."); // Codename One/JavaSE initialization may leave non-daemon threads running. // Force a clean exit so CI jobs don't hang after successful completion. @@ -309,6 +310,66 @@ public void log(String message) { "Raw 'Generated instance dispatch' message should be replaced, got: " + summary); } + /** Regression: when a lambda references an unresolved symbol (e.g. a + * missing import for {@code com.codename1.io.Util}), the initial + * script evaluation succeeds because the body is only re-evaluated + * when the event fires. The user types into the field, the EDT + * catches the resulting EvalError, and the UI silently stops + * responding. The runner now captures a {@link + * PlaygroundContext.RuntimeErrorReporter} into each created lambda so + * the later failure surfaces back to the editor. */ + private static void smokeLambdaRuntimeErrorSurfacesToReporter() { + Display.init(null); + + Form host = new Form("Host", new BorderLayout()); + Container preview = new Container(new BorderLayout()); + host.add(BorderLayout.CENTER, preview); + host.show(); + + final List reported = new ArrayList(); + PlaygroundContext context = new PlaygroundContext(host, preview, null, + new PlaygroundContext.Logger() { + public void log(String message) { + } + }, + new PlaygroundContext.RuntimeErrorReporter() { + public void reportRuntimeError(String message, Throwable cause) { + reported.add(message); + } + }); + + PlaygroundRunner runner = new PlaygroundRunner(); + // The lambda references DefinitelyMissingType (no matching import or + // class). Initial eval must still produce the Label — only the + // lambda body's later evaluation fails. + PlaygroundRunner.RunResult result = runner.run( + "import com.codename1.ui.*;\n" + + "Runnable r = () -> DefinitelyMissingType.doStuff();\n" + + "Label hi = new Label(\"OK\");\n" + + "hi.putClientProperty(\"r\", r);\n" + + "hi;\n", + context); + + require(result.getComponent() instanceof Label, + "Script with unresolved lambda body should still build the UI: " + summarizeMessages(result)); + Object stored = ((Label) result.getComponent()).getClientProperty("r"); + require(stored instanceof Runnable, "Lambda should be assignable to Runnable"); + require(reported.isEmpty(), "Reporter should not fire until the lambda is actually invoked"); + + try { + ((Runnable) stored).run(); + require(false, "Invoking the failing lambda should propagate a RuntimeException"); + } catch (RuntimeException expected) { + // expected — EDT would have swallowed this in production + } + + require(reported.size() == 1, + "Runtime error reporter should fire exactly once for the failing lambda, saw: " + reported); + String msg = reported.get(0); + require(msg != null && msg.indexOf("DefinitelyMissingType") >= 0, + "Runtime error message should name the unresolved symbol, got: " + msg); + } + private static void require(boolean condition, String message) { if (!condition) { throw new IllegalStateException(message);