From 45d0a13c9b1abd83e9fd84214575e5f72d244090 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 26 Nov 2018 15:58:51 +0300 Subject: [PATCH] In HtmlUnit, decode stack traces for all constructed exceptions --- .../teavm/classlib/java/lang/TThrowable.java | 7 +- .../backend/javascript/JavaScriptTarget.java | 35 ++++++-- .../javascript/rendering/RuntimeRenderer.java | 26 ++++++ .../org/teavm/backend/javascript/runtime.js | 12 ++- .../org/teavm/junit/HtmlUnitRunStrategy.java | 84 +++++++++++++++++-- .../org/teavm/junit/RhinoResultParser.java | 16 ++-- .../java/org/teavm/junit/TestEntryPoint.java | 2 +- .../main/resources/teavm-htmlunit-adapter.js | 5 +- .../main/resources/teavm-selenium-adapter.js | 36 -------- .../src/main/resources/teavm-selenium.js | 55 ------------ 10 files changed, 162 insertions(+), 116 deletions(-) delete mode 100644 tools/junit/src/main/resources/teavm-selenium-adapter.js delete mode 100644 tools/junit/src/main/resources/teavm-selenium.js diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java index b1d80f812..a7c90e59a 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java @@ -154,7 +154,12 @@ public class TThrowable extends RuntimeException { } public void printStackTrace(TPrintStream stream) { - stream.println(TString.wrap(getClass().getName() + ": " + getMessage())); + stream.print(TString.wrap(getClass().getName())); + String message = getMessage(); + if (message != null) { + stream.print(TString.wrap(": " + getMessage())); + } + stream.println(); if (stackTrace != null) { for (TStackTraceElement element : stackTrace) { stream.print(TString.wrap(" at ")); diff --git a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java index 57848fe83..44093d292 100644 --- a/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java +++ b/core/src/main/java/org/teavm/backend/javascript/JavaScriptTarget.java @@ -57,6 +57,7 @@ import org.teavm.debugging.information.DummyDebugInformationEmitter; import org.teavm.debugging.information.SourceLocation; import org.teavm.dependency.DependencyAnalyzer; import org.teavm.dependency.DependencyListener; +import org.teavm.dependency.DependencyType; import org.teavm.dependency.MethodDependency; import org.teavm.interop.PlatformMarker; import org.teavm.interop.PlatformMarkers; @@ -200,6 +201,8 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { public void contributeDependencies(DependencyAnalyzer dependencyAnalyzer) { MethodDependency dep; + DependencyType stringType = dependencyAnalyzer.getType("java.lang.String"); + dep = dependencyAnalyzer.linkMethod(new MethodReference(Class.class.getName(), "getClass", ValueType.object("org.teavm.platform.PlatformClass"), ValueType.parse(Class.class)), null); dep.getVariable(0).propagate(dependencyAnalyzer.getType("org.teavm.platform.PlatformClass")); @@ -208,23 +211,23 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { dep = dependencyAnalyzer.linkMethod(new MethodReference(String.class, "", char[].class, void.class), null); - dep.getVariable(0).propagate(dependencyAnalyzer.getType("java.lang.String")); + dep.getVariable(0).propagate(stringType); dep.getVariable(1).propagate(dependencyAnalyzer.getType("[C")); dep.use(); dep = dependencyAnalyzer.linkMethod(new MethodReference(String.class, "getChars", int.class, int.class, char[].class, int.class, void.class), null); - dep.getVariable(0).propagate(dependencyAnalyzer.getType("java.lang.String")); + dep.getVariable(0).propagate(stringType); dep.getVariable(3).propagate(dependencyAnalyzer.getType("[C")); dep.use(); MethodDependency internDep = dependencyAnalyzer.linkMethod(new MethodReference(String.class, "intern", String.class), null); - internDep.getVariable(0).propagate(dependencyAnalyzer.getType("java.lang.String")); + internDep.getVariable(0).propagate(stringType); internDep.use(); dep = dependencyAnalyzer.linkMethod(new MethodReference(String.class, "length", int.class), null); - dep.getVariable(0).propagate(dependencyAnalyzer.getType("java.lang.String")); + dep.getVariable(0).propagate(stringType); dep.use(); dependencyAnalyzer.linkMethod(new MethodReference(Object.class, "clone", Object.class), null).use(); @@ -240,23 +243,39 @@ public class JavaScriptTarget implements TeaVMTarget, TeaVMJavaScriptHost { dep.use(); exceptionCons.getVariable(0).propagate(dependencyAnalyzer.getType(NoClassDefFoundError.class.getName())); - exceptionCons.getVariable(1).propagate(dependencyAnalyzer.getType("java.lang.String")); + exceptionCons.getVariable(1).propagate(stringType); exceptionCons = dependencyAnalyzer.linkMethod(new MethodReference(NoSuchFieldError.class, "", String.class, void.class), null); exceptionCons.use(); exceptionCons.getVariable(0).propagate(dependencyAnalyzer.getType(NoSuchFieldError.class.getName())); - exceptionCons.getVariable(1).propagate(dependencyAnalyzer.getType("java.lang.String")); + exceptionCons.getVariable(1).propagate(stringType); exceptionCons = dependencyAnalyzer.linkMethod(new MethodReference(NoSuchMethodError.class, "", String.class, void.class), null); exceptionCons.use(); exceptionCons.getVariable(0).propagate(dependencyAnalyzer.getType(NoSuchMethodError.class.getName())); - exceptionCons.getVariable(1).propagate(dependencyAnalyzer.getType("java.lang.String")); + exceptionCons.getVariable(1).propagate(stringType); exceptionCons = dependencyAnalyzer.linkMethod(new MethodReference( RuntimeException.class, "", String.class, void.class), null); exceptionCons.getVariable(0).propagate(dependencyAnalyzer.getType(RuntimeException.class.getName())); - exceptionCons.getVariable(1).propagate(dependencyAnalyzer.getType("java.lang.String")); + exceptionCons.getVariable(1).propagate(stringType); exceptionCons.use(); + + dep = dependencyAnalyzer.linkMethod(new MethodReference( + StackTraceElement.class, "", String.class, String.class, String.class, + int.class, void.class), null); + dep.getVariable(0).propagate(dependencyAnalyzer.getType(StackTraceElement.class.getName())); + dep.getVariable(1).propagate(stringType); + dep.getVariable(2).propagate(stringType); + dep.getVariable(3).propagate(stringType); + dep.use(); + + dep = dependencyAnalyzer.linkMethod(new MethodReference( + Throwable.class, "setStackTrace", StackTraceElement[].class, void.class), null); + dep.getVariable(0).propagate(dependencyAnalyzer.getType(Throwable.class.getName())); + dep.getVariable(1).propagate(dependencyAnalyzer.getType("[Ljava/lang/StackTraceElement;")); + dep.getVariable(1).getArrayItem().propagate(dependencyAnalyzer.getType(StackTraceElement.class.getName())); + dep.use(); } @Override diff --git a/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java b/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java index e88c58969..e96d2e611 100644 --- a/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java +++ b/core/src/main/java/org/teavm/backend/javascript/rendering/RuntimeRenderer.java @@ -51,6 +51,8 @@ public class RuntimeRenderer { renderRuntimeIntern(); renderRuntimeThreads(); renderRuntimeCreateException(); + renderCreateStackTraceElement(); + renderSetStackTrace(); } catch (NamingException e) { throw new RenderingException("Error rendering runtime methods. See a cause for details", e); } catch (IOException e) { @@ -170,4 +172,28 @@ public class RuntimeRenderer { writer.append("(message);").softNewLine(); writer.outdent().append("}").newLine(); } + + private void renderCreateStackTraceElement() throws IOException { + writer.append("function $rt_createStackElement(") + .append("className,").ws() + .append("methodName,").ws() + .append("fileName,").ws() + .append("lineNumber)").ws().append("{").indent().softNewLine(); + writer.append("return "); + writer.append(writer.getNaming().getNameForInit(new MethodReference(StackTraceElement.class, + "", String.class, String.class, String.class, int.class, void.class))); + writer.append("(className,").ws() + .append("methodName,").ws() + .append("fileName,").ws() + .append("lineNumber);").softNewLine(); + writer.outdent().append("}").newLine(); + } + + private void renderSetStackTrace() throws IOException { + writer.append("function $rt_setStack(e,").ws().append("stack)").ws().append("{").indent().softNewLine(); + writer.appendMethodBody(new MethodReference(Throwable.class, "setStackTrace", StackTraceElement[].class, + void.class)); + writer.append("(e,").ws().append("stack);").softNewLine(); + writer.outdent().append("}").newLine(); + } } diff --git a/core/src/main/resources/org/teavm/backend/javascript/runtime.js b/core/src/main/resources/org/teavm/backend/javascript/runtime.js index f639d4851..7b57abd59 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/runtime.js +++ b/core/src/main/resources/org/teavm/backend/javascript/runtime.js @@ -228,6 +228,16 @@ function $rt_exception(ex) { } err.$javaException = ex; ex.$jsException = err; + if (typeof $rt_decodeStack === "function" && err.stack) { + var stack = $rt_decodeStack(err.stack); + var javaStack = $rt_createArray($rt_objcls(), stack.length); + for (var i = 0; i < stack.length; ++i) { + var element = stack[i]; + javaStack.data[i] = $rt_createStackElement($rt_str(element.className), + $rt_str(element.methodName), $rt_str(element.fileName), element.lineNumber); + } + $rt_setStack(ex, javaStack); + } } return err; } @@ -393,7 +403,7 @@ var $rt_stderrBuffer = ""; var $rt_putStderr = typeof $rt_putStderrCustom === "function" ? $rt_putStderrCustom : function(ch) { if (ch === 0xA) { if (console) { - console.info($rt_stderrBuffer); + console.error($rt_stderrBuffer); } $rt_stderrBuffer = ""; } else { diff --git a/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java b/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java index 76478e65f..d733bb974 100644 --- a/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java +++ b/tools/junit/src/main/java/org/teavm/junit/HtmlUnitRunStrategy.java @@ -18,6 +18,7 @@ package org.teavm.junit; import com.gargoylesoftware.htmlunit.BrowserVersion; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebConsole; import com.gargoylesoftware.htmlunit.WebWindow; import com.gargoylesoftware.htmlunit.html.HtmlPage; import java.io.File; @@ -26,7 +27,10 @@ import java.io.IOException; import java.io.InputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import net.sourceforge.htmlunit.corejs.javascript.BaseFunction; +import net.sourceforge.htmlunit.corejs.javascript.Context; import net.sourceforge.htmlunit.corejs.javascript.Function; +import net.sourceforge.htmlunit.corejs.javascript.NativeArray; import net.sourceforge.htmlunit.corejs.javascript.NativeJavaObject; import net.sourceforge.htmlunit.corejs.javascript.Scriptable; import org.apache.commons.io.IOUtils; @@ -59,17 +63,33 @@ class HtmlUnitRunStrategy implements TestRunStrategy { } catch (IOException e) { throw new RuntimeException(e); } - page.get().executeJavaScript(readFile(new File(run.getBaseDirectory(), run.getFileName()))); + HtmlPage pageRef = page.get(); + + pageRef.executeJavaScript(readFile(new File(run.getBaseDirectory(), run.getFileName()))); + boolean decodeStack = Boolean.parseBoolean(System.getProperty(TeaVMTestRunner.JS_DECODE_STACK, "true")); + File debugFile = decodeStack ? new File(run.getBaseDirectory(), run.getFileName() + ".teavmdbg") : null; + RhinoResultParser resultParser = new RhinoResultParser(debugFile); AsyncResult asyncResult = new AsyncResult(); Function function = (Function) page.get().executeJavaScript(readResource("teavm-htmlunit-adapter.js")) .getJavaScriptResult(); - Object[] args = new Object[] { new NativeJavaObject(function, asyncResult, AsyncResult.class) }; - page.get().executeJavaScriptFunctionIfPossible(function, function, args, page.get()); + Object[] args = new Object[] { + decodeStack ? createStackDecoderFunction(resultParser) : null, + new NativeJavaObject(function, asyncResult, AsyncResult.class) + }; + pageRef.executeJavaScriptFunctionIfPossible(function, function, args, page.get()); - boolean decodeStack = Boolean.parseBoolean(System.getProperty(TeaVMTestRunner.JS_DECODE_STACK, "true")); - File debugFile = decodeStack ? new File(run.getBaseDirectory(), run.getFileName() + ".teavmdbg") : null; - RhinoResultParser.parseResult((Scriptable) asyncResult.getResult(), run.getCallback(), debugFile); + resultParser.parseResult((Scriptable) asyncResult.getResult(), run.getCallback()); + } + + private Function createStackDecoderFunction(RhinoResultParser resultParser) { + return new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + String stack = args[0].toString(); + return new NativeArray(resultParser.decodeStack(stack)); + } + }; } private void cleanUp() { @@ -86,7 +106,57 @@ class HtmlUnitRunStrategy implements TestRunStrategy { } private void init() { - webClient.set(new WebClient(BrowserVersion.CHROME)); + WebClient client = new WebClient(BrowserVersion.CHROME); + client.getWebConsole().setLogger(new WebConsole.Logger() { + @Override + public boolean isTraceEnabled() { + return false; + } + + @Override + public void trace(Object message) { + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void debug(Object message) { + } + + @Override + public boolean isInfoEnabled() { + return true; + } + + @Override + public void info(Object message) { + System.out.println(message); + } + + @Override + public boolean isWarnEnabled() { + return true; + } + + @Override + public void warn(Object message) { + System.out.println(message); + } + + @Override + public boolean isErrorEnabled() { + return true; + } + + @Override + public void error(Object message) { + System.err.println(message); + } + }); + webClient.set(client); } private String readFile(File file) throws IOException { diff --git a/tools/junit/src/main/java/org/teavm/junit/RhinoResultParser.java b/tools/junit/src/main/java/org/teavm/junit/RhinoResultParser.java index 49ece5d32..aa6d7010d 100644 --- a/tools/junit/src/main/java/org/teavm/junit/RhinoResultParser.java +++ b/tools/junit/src/main/java/org/teavm/junit/RhinoResultParser.java @@ -36,11 +36,17 @@ import org.teavm.model.MethodReference; final class RhinoResultParser { private static Pattern pattern = Pattern.compile("(([A-Za-z_$]+)\\(\\))?@.+:([0-9]+)"); private static Pattern lineSeparator = Pattern.compile("\\r\\n|\r|\n"); + private File debugFile; + private DebugInformation debugInformation; + private String[] script; - private RhinoResultParser() { + RhinoResultParser(File debugFile) { + debugInformation = debugFile != null ? getDebugInformation(debugFile) : null; + script = getScript(new File(debugFile.getParentFile(), + debugFile.getName().substring(0, debugFile.getName().length() - 9))); } - static void parseResult(Scriptable result, TestRunCallback callback, File debugFile) { + void parseResult(Scriptable result, TestRunCallback callback) { if (result == null) { callback.complete(); return; @@ -65,10 +71,8 @@ final class RhinoResultParser { String stack = result.get("stack", result).toString(); StackTraceElement[] decodedStack = null; if (debugInformation != null) { - String[] script = getScript(new File(debugFile.getParentFile(), - debugFile.getName().substring(0, debugFile.getName().length() - 9))); List elements = new ArrayList<>(); - elements.addAll(Arrays.asList(decodeStack(stack, script, debugInformation))); + elements.addAll(Arrays.asList(decodeStack(stack))); List currentElements = Arrays.asList(Thread.currentThread().getStackTrace()); elements.addAll(currentElements.subList(2, currentElements.size())); decodedStack = elements.toArray(new StackTraceElement[0]); @@ -92,7 +96,7 @@ final class RhinoResultParser { } } - private static StackTraceElement[] decodeStack(String stack, String[] script, DebugInformation debugInformation) { + StackTraceElement[] decodeStack(String stack) { List elements = new ArrayList<>(); for (String line : lineSeparator.split(stack)) { Matcher matcher = pattern.matcher(line); diff --git a/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java b/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java index 89590228c..bceb69248 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestEntryPoint.java @@ -21,7 +21,7 @@ final class TestEntryPoint { private TestEntryPoint() { } - public static void run() throws Throwable { + public static void run() { before(); try { launchTest(); diff --git a/tools/junit/src/main/resources/teavm-htmlunit-adapter.js b/tools/junit/src/main/resources/teavm-htmlunit-adapter.js index 798869dcf..384b7b28e 100644 --- a/tools/junit/src/main/resources/teavm-htmlunit-adapter.js +++ b/tools/junit/src/main/resources/teavm-htmlunit-adapter.js @@ -1,4 +1,7 @@ -function runMain(callback) { +var $rt_decodeStack; + +function runMain(stackDecoder, callback) { + $rt_decodeStack = stackDecoder; main([], function(result) { var message = {}; if (result instanceof Error) { diff --git a/tools/junit/src/main/resources/teavm-selenium-adapter.js b/tools/junit/src/main/resources/teavm-selenium-adapter.js deleted file mode 100644 index 9b77aa3dd..000000000 --- a/tools/junit/src/main/resources/teavm-selenium-adapter.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016 Alexey Andreev. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -main([], function(result) { - var message = {}; - if (result instanceof Error) { - makeErrorMessage(message, result); - } else { - message.status = "ok"; - } - window.parent.postMessage(JSON.stringify(message), "*"); -}); - -function makeErrorMessage(message, e) { - message.status = "exception"; - var stack = ""; - if (e.$javaException && e.$javaException.constructor.$meta) { - stack = e.$javaException.constructor.$meta.name + ": "; - stack += e.$javaException.getMessage() || ""; - stack += "\n"; - } - message.stack = stack + e.stack; -} \ No newline at end of file diff --git a/tools/junit/src/main/resources/teavm-selenium.js b/tools/junit/src/main/resources/teavm-selenium.js deleted file mode 100644 index 94395da4f..000000000 --- a/tools/junit/src/main/resources/teavm-selenium.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2016 Alexey Andreev. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var runtimeSource = arguments[0]; -var testSource = arguments[1]; -var adapterSource = arguments[2]; -var seleniumCallback = arguments[arguments.length - 1]; - -var iframe = document.createElement("iframe"); -document.body.appendChild(iframe); -var doc = iframe.contentDocument; - -window.jsErrors = []; -window.onerror = reportError; -iframe.contentWindow.onerror = reportError; - -loadScripts([ runtimeSource, testSource, adapterSource ]); -window.addEventListener("message", handleMessage); - -function handleMessage(event) { - window.removeEventListener("message", handleMessage); - document.body.removeChild(iframe); - seleniumCallback(event.data) -} - -function loadScripts(scripts) { - for (var i = 0; i < scripts.length; ++i) { - var elem = doc.createElement("script"); - elem.type = "text/javascript"; - doc.head.appendChild(elem); - elem.text = scripts[i]; - } -} -function reportError(error, url, line) { - window.jsErrors.push(error + " at " + line) -} -function report(error) { - window.jsErrors.push(error) -} -function globalEval(window, arg) { - eval.apply(window, [arg]) -} \ No newline at end of file