diff --git a/man/rhino.1 b/man/rhino.1 index 5b35088aea..4d5ce7a0d5 100644 --- a/man/rhino.1 +++ b/man/rhino.1 @@ -80,6 +80,34 @@ creates a synchronized function (in the sense of a Java synchronized method) fro Quit shell. The shell will also quit in interactive mode if an end-of-file character is typed at the prompt. .IP version(\fI[number]\fP) Get or set JavaScript version number. If no argument is supplied, the current version number is returned. If an argument is supplied, it is expected to be one of 100, 110, 120, 130, or 140 to indicate JavaScript version 1.0, 1.1, 1.2, 1.3, or 1.4 respectively. +.IP console +The console object provides a simple debugging console similar to the console object provided by web browsers. +.RS +.IP console.log(\fIformat[arg,\&.\&.\&.]\fP) +For general output of logging information. String substitution and additional arguments are supported. Prints formatted text according to the format and args supplied with "INFO" prefix. This function is identical to console.info(\fIformat\fP[arg,\&.\&.\&.]). +.IP console.trace(\fIformat[arg,\&.\&.\&.]\fP) +This function is identical to console.log(\fIformat\fP[arg,\&.\&.\&.]) except it prints "TRACE" prefix instead of "INFO". +.IP console.debug(\fIformat[arg,\&.\&.\&.]\fP) +This function is identical to console.log(\fIformat\fP[arg,\&.\&.\&.]) except it prints "DEBUG" prefix instead of "INFO". +.IP console.info(\fIformat[arg,\&.\&.\&.]\fP) +This function is identical to console.log(\fIformat\fP[arg,\&.\&.\&.]). +.IP console.warn(\fIformat[arg,\&.\&.\&.]\fP) +This function is identical to console.log(\fIformat\fP[arg,\&.\&.\&.]) except it prints "WARN" prefix instead of "INFO". +.IP console.error(\fIformat[arg,\&.\&.\&.]\fP) +This function is identical to console.log(\fIformat\fP[arg,\&.\&.\&.]) except it prints "ERROR" prefix instead of "INFO". +.IP console.assert(\fIexpression[arg,\&.\&.\&.]\fP) +Prints error if expression is false. If args are supplied, they will be printed also. +.IP console.count(\fI[label]\fP) +Increases the counter of label by one which starts from zero and prints the label and value after increment. If label is not supplied, "default" is the label. +.IP console.countReset(\fI[label]\fP) +Resets the counter of label to zero. If label is not supplied, "default" is the label. +.IP console.time(\fI[label]\fP) +Starts a timer of label. Use console.timeEnd(\fI[label]\fP) to stop the timer and print the elapsed time. Use console.timeLog(\fI[label]\fP) to print the elapsed time without stopping the timer. +.IP console.timeLog(\fI[label]\fP) +See console.time(\fI[label]\fP). +.IP console.timeEnd(\fI[label]\fP) +See console.time(\fI[label]\fP). +.RE .SH SEE ALSO The online documentation under diff --git a/src/org/mozilla/javascript/NativeConsole.java b/src/org/mozilla/javascript/NativeConsole.java new file mode 100644 index 0000000000..e9c1294b45 --- /dev/null +++ b/src/org/mozilla/javascript/NativeConsole.java @@ -0,0 +1,459 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NativeConsole extends IdScriptableObject { + private static final long serialVersionUID = 5694613212458273057L; + + private static final Object CONSOLE_TAG = "Console"; + + private static final String DEFAULT_LABEL = "default"; + + private static final Pattern FMT_REG = Pattern.compile("%[sfdioO%]"); + + private final Map timers = new ConcurrentHashMap<>(); + + private final Map counters = new ConcurrentHashMap<>(); + + private final ConsolePrinter printer; + + public enum Level { + TRACE, + DEBUG, + INFO, + WARN, + ERROR + } + + public interface ConsolePrinter extends Serializable { + void print( + Context cx, + Scriptable scope, + Level level, + Object[] args, + ScriptStackElement[] stack); + } + + public static void init(Scriptable scope, boolean sealed, ConsolePrinter printer) { + NativeConsole obj = new NativeConsole(printer); + obj.activatePrototypeMap(MAX_ID); + obj.setPrototype(getObjectPrototype(scope)); + obj.setParentScope(scope); + if (sealed) { + obj.sealObject(); + } + ScriptableObject.defineProperty(scope, "console", obj, ScriptableObject.DONTENUM); + } + + private NativeConsole(ConsolePrinter printer) { + this.printer = printer; + } + + @Override + public String getClassName() { + return "Console"; + } + + @Override + protected void initPrototypeId(int id) { + if (id > LAST_METHOD_ID) { + throw new IllegalStateException(String.valueOf(id)); + } + + String name; + int arity; + switch (id) { + case Id_toSource: + arity = 0; + name = "toSource"; + break; + case Id_trace: + arity = 1; + name = "trace"; + break; + case Id_debug: + arity = 1; + name = "debug"; + break; + case Id_log: + arity = 1; + name = "log"; + break; + case Id_info: + arity = 1; + name = "info"; + break; + case Id_warn: + arity = 1; + name = "warn"; + break; + case Id_error: + arity = 1; + name = "error"; + break; + case Id_assert: + arity = 2; + name = "assert"; + break; + case Id_count: + arity = 1; + name = "count"; + break; + case Id_countReset: + arity = 1; + name = "countReset"; + break; + case Id_time: + arity = 1; + name = "time"; + break; + case Id_timeEnd: + arity = 1; + name = "timeEnd"; + break; + case Id_timeLog: + arity = 2; + name = "timeLog"; + break; + default: + throw new IllegalStateException(String.valueOf(id)); + } + initPrototypeMethod(CONSOLE_TAG, id, name, arity); + } + + @Override + public Object execIdCall( + IdFunctionObject f, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if (!f.hasTag(CONSOLE_TAG)) { + return super.execIdCall(f, cx, scope, thisObj, args); + } + + int methodId = f.methodId(); + switch (methodId) { + case Id_toSource: + return "Console"; + + case Id_trace: + { + ScriptStackElement[] stack = + new EvaluatorException("[object Object]").getScriptStack(); + printer.print(cx, scope, Level.TRACE, args, stack); + break; + } + + case Id_debug: + printer.print(cx, scope, Level.DEBUG, args, null); + break; + + case Id_log: + case Id_info: + printer.print(cx, scope, Level.INFO, args, null); + break; + + case Id_warn: + printer.print(cx, scope, Level.WARN, args, null); + break; + + case Id_error: + printer.print(cx, scope, Level.ERROR, args, null); + break; + + case Id_assert: + jsAssert(cx, scope, args); + break; + + case Id_count: + count(cx, scope, args); + break; + + case Id_countReset: + countReset(cx, scope, args); + break; + + case Id_time: + time(cx, scope, args); + break; + + case Id_timeEnd: + timeEnd(cx, scope, args); + break; + + case Id_timeLog: + timeLog(cx, scope, args); + break; + + default: + throw new IllegalStateException(String.valueOf(methodId)); + } + + return Undefined.instance; + } + + private void print(Context cx, Scriptable scope, Level level, String msg) { + printer.print(cx, scope, level, new String[] {msg}, null); + } + + public static String format(Context cx, Scriptable scope, Object[] args) { + String msg = ScriptRuntime.toString(args[0]); + if (msg == null || msg.length() == 0) { + return ""; + } + + int argIndex = 1; + Matcher matcher = FMT_REG.matcher(msg); + StringBuffer buffer = new StringBuffer(msg.length() * 2); + while (matcher.find()) { + String placeHolder = matcher.group(); + String replaceArg; + + if (placeHolder.equals("%%")) { + replaceArg = "%"; + } else if (argIndex >= args.length) { + replaceArg = placeHolder; + argIndex++; + } else { + Object val = args[argIndex]; + switch (placeHolder) { + case "%s": + replaceArg = formatString(val); + break; + + case "%d": + case "%i": + replaceArg = formatInt(val); + break; + + case "%f": + replaceArg = formatFloat(val); + break; + + case "%o": + case "%O": + replaceArg = formatObj(cx, scope, val); + break; + + default: + replaceArg = ""; + break; + } + argIndex++; + } + + matcher.appendReplacement(buffer, Matcher.quoteReplacement(replaceArg)); + } + matcher.appendTail(buffer); + + return buffer.toString(); + } + + private static String formatString(Object val) { + if (val instanceof BigInteger) { + return ScriptRuntime.toString(val) + "n"; + } + + if (ScriptRuntime.isSymbol(val)) { + return val.toString(); + } + + return ScriptRuntime.toString(val); + } + + private static String formatInt(Object val) { + if (val instanceof BigInteger) { + return ScriptRuntime.bigIntToString((BigInteger) val, 10) + "n"; + } + + if (ScriptRuntime.isSymbol(val)) { + return ScriptRuntime.NaNobj.toString(); + } + + double number = ScriptRuntime.toNumber(val); + + if (Double.isInfinite(number) || Double.isNaN(number)) { + return ScriptRuntime.toString(number); + } + + return String.valueOf((long) number); + } + + private static String formatFloat(Object val) { + if (val instanceof BigInteger || ScriptRuntime.isSymbol(val)) { + return ScriptRuntime.NaNobj.toString(); + } + + return ScriptRuntime.numberToString(ScriptRuntime.toNumber(val), 10); + } + + private static String formatObj(Context cx, Scriptable scope, Object arg) { + if (arg == null) { + return "null"; + } + + if (Undefined.isUndefined(arg)) { + return Undefined.SCRIPTABLE_UNDEFINED.toString(); + } + + try { + Object stringify = NativeJSON.stringify(cx, scope, arg, null, null); + return ScriptRuntime.toString(stringify); + } catch (EcmaError e) { + if ("TypeError".equals(e.getName())) { + // Fall back to use ScriptRuntime.toString() in some case such as + // NativeJSON.stringify not support BigInt yet. + return ScriptRuntime.toString(arg); + } + throw e; + } + } + + private void jsAssert(Context cx, Scriptable scope, Object[] args) { + if (args != null && args.length > 0 && ScriptRuntime.toBoolean(args[0])) { + return; + } + + StringBuilder msg = new StringBuilder("Assertion failed:"); + if (args != null && args.length > 1) { + for (int i = 1; i < args.length; ++i) { + msg.append(" ").append(ScriptRuntime.toString(args[i])); + } + } else { + msg.append(" console.assert"); + } + + print(cx, scope, Level.ERROR, msg.toString()); + } + + private void count(Context cx, Scriptable scope, Object[] args) { + String label = args.length > 0 ? ScriptRuntime.toString(args[0]) : DEFAULT_LABEL; + int count = counters.computeIfAbsent(label, l -> new AtomicInteger(0)).incrementAndGet(); + print(cx, scope, Level.INFO, label + ": " + count); + } + + private void countReset(Context cx, Scriptable scope, Object[] args) { + String label = args.length > 0 ? ScriptRuntime.toString(args[0]) : DEFAULT_LABEL; + AtomicInteger counter = counters.remove(label); + if (counter == null) { + print(cx, scope, Level.WARN, "Count for '" + label + "' does not exist."); + } + } + + private void time(Context cx, Scriptable scope, Object[] args) { + String label = args.length > 0 ? ScriptRuntime.toString(args[0]) : DEFAULT_LABEL; + Long start = timers.get(label); + if (start != null) { + print(cx, scope, Level.WARN, "Timer '" + label + "' already exists."); + return; + } + timers.put(label, System.nanoTime()); + } + + private void timeEnd(Context cx, Scriptable scope, Object[] args) { + String label = args.length > 0 ? ScriptRuntime.toString(args[0]) : DEFAULT_LABEL; + Long start = timers.remove(label); + if (start == null) { + print(cx, scope, Level.WARN, "Timer '" + label + "' does not exist."); + return; + } + print(cx, scope, Level.INFO, label + ": " + nano2Milli(System.nanoTime() - start) + "ms"); + } + + private void timeLog(Context cx, Scriptable scope, Object[] args) { + String label = args.length > 0 ? ScriptRuntime.toString(args[0]) : DEFAULT_LABEL; + Long start = timers.get(label); + if (start == null) { + print(cx, scope, Level.WARN, "Timer '" + label + "' does not exist."); + return; + } + StringBuilder msg = + new StringBuilder(label + ": " + nano2Milli(System.nanoTime() - start) + "ms"); + + if (args.length > 1) { + for (int i = 1; i < args.length; i++) { + msg.append(" ").append(ScriptRuntime.toString(args[i])); + } + } + print(cx, scope, Level.INFO, msg.toString()); + } + + private double nano2Milli(Long nano) { + return nano / 1000000D; + } + + @Override + protected int findPrototypeId(String s) { + int id; + switch (s) { + case "log": + id = Id_log; + break; + case "info": + id = Id_info; + break; + case "time": + id = Id_time; + break; + case "warn": + id = Id_warn; + break; + case "count": + id = Id_count; + break; + case "debug": + id = Id_debug; + break; + case "error": + id = Id_error; + break; + case "trace": + id = Id_trace; + break; + case "assert": + id = Id_assert; + break; + case "timeEnd": + id = Id_timeEnd; + break; + case "timeLog": + id = Id_timeLog; + break; + case "toSource": + id = Id_toSource; + break; + case "countReset": + id = Id_countReset; + break; + default: + id = 0; + break; + } + return id; + } + + private static final int Id_toSource = 1, + Id_trace = 2, + Id_debug = 3, + Id_log = 4, + Id_info = 5, + Id_warn = 6, + Id_error = 7, + Id_assert = 8, + Id_count = 9, + Id_countReset = 10, + Id_time = 11, + Id_timeEnd = 12, + Id_timeLog = 13, + LAST_METHOD_ID = 13, + MAX_ID = 13; +} diff --git a/testsrc/org/mozilla/javascript/tests/NativeConsoleTest.java b/testsrc/org/mozilla/javascript/tests/NativeConsoleTest.java new file mode 100644 index 0000000000..d86e80becb --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/NativeConsoleTest.java @@ -0,0 +1,371 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.javascript.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; +import org.junit.Assert; +import org.junit.Test; +import org.mozilla.javascript.*; +import org.mozilla.javascript.NativeConsole.Level; + +/** Test NativeConsole */ +public class NativeConsoleTest { + + private static class PrinterCall { + public Level level; + public Object[] args; + public ScriptStackElement[] stack; + + public PrinterCall(Level level, Object[] args, ScriptStackElement[] stack) { + this.level = level; + this.args = args; + this.stack = stack; + } + + public PrinterCall(Level level, Object[] args) { + this(level, args, null); + } + + public void assertEquals(PrinterCall expectedCall) { + Assert.assertEquals(expectedCall.level, this.level); + if (expectedCall.args != null) { + Assert.assertEquals(expectedCall.args.length, this.args.length); + for (int i = 0; i < expectedCall.args.length; ++i) { + if (expectedCall.args[i] instanceof Pattern && this.args[i] instanceof String) { + Assert.assertTrue( + "\"" + + this.args[i] + + "\" does not matches \"" + + expectedCall.args[i] + + "\"", + ((Pattern) expectedCall.args[i]) + .matcher((String) this.args[i]) + .matches()); + } else { + Assert.assertEquals(expectedCall.args[i], this.args[i]); + } + } + } + if (expectedCall.stack != null) { + Assert.assertEquals(expectedCall.stack.length, this.stack.length); + for (int i = 0; i < expectedCall.stack.length; ++i) { + Assert.assertEquals(expectedCall.stack[i].fileName, this.stack[i].fileName); + Assert.assertEquals( + expectedCall.stack[i].functionName, this.stack[i].functionName); + Assert.assertEquals(expectedCall.stack[i].lineNumber, this.stack[i].lineNumber); + } + } + } + } + + private static class DummyConsolePrinter implements NativeConsole.ConsolePrinter { + public List calls = new ArrayList<>(); + + @Override + public void print( + Context cx, + Scriptable scope, + Level level, + Object[] args, + ScriptStackElement[] stack) { + calls.add(new PrinterCall(level, args, stack)); + } + + public void clear() { + calls.clear(); + } + + public void assertCalls(List expectedCalls) { + assertEquals(expectedCalls.size(), calls.size()); + for (int i = 0; i < calls.size(); ++i) { + calls.get(i).assertEquals(expectedCalls.get(i)); + } + } + } + + @Test + public void testFormatPercentSign() { + assertFormat(new Object[] {"%%"}, "%"); + assertFormat(new Object[] {"a%%"}, "a%"); + assertFormat(new Object[] {"%%b"}, "%b"); + assertFormat(new Object[] {"a%%b"}, "a%b"); + assertFormat(new Object[] {"a%%%%b"}, "a%%b"); + assertFormat(new Object[] {"a%%c%%b"}, "a%c%b"); + } + + @Test + public void testFormatString() { + assertFormat(new Object[] {"%s", "abc"}, "abc"); + assertFormat(new Object[] {"%s", 100}, "100"); + assertFormat(new Object[] {"%s", 100.1D}, "100.1"); + assertFormat(new Object[] {"%s", Integer.MAX_VALUE}, String.valueOf(Integer.MAX_VALUE)); + assertFormat(new Object[] {"%s", Integer.MIN_VALUE}, String.valueOf(Integer.MIN_VALUE)); + assertFormat(new Object[] {"%s", Long.MAX_VALUE}, "9223372036854776000"); + assertFormat(new Object[] {"%s", Long.MIN_VALUE}, "-9223372036854776000"); + + assertFormat(new Object[] {"%s", Double.NaN}, "NaN"); + assertFormat(new Object[] {"%s", Double.POSITIVE_INFINITY}, "Infinity"); + assertFormat(new Object[] {"%s", Double.NEGATIVE_INFINITY}, "-Infinity"); + assertFormat(new Object[] {"%s", Undefined.instance}, "undefined"); + assertFormat(new Object[] {"%s", SymbolKey.ITERATOR}, "Symbol(Symbol.iterator)"); + + assertFormat(new Object[] {"%s", BigInteger.valueOf(100)}, "100n"); + assertFormat( + new Object[] {"%s", new BigInteger("1234567890123456789012345678901234567890")}, + "1234567890123456789012345678901234567890n"); + + assertFormat(new Object[] {"a%s", "abc"}, "aabc"); + assertFormat(new Object[] {"%sb", "abc"}, "abcb"); + assertFormat(new Object[] {"a%sb", "abc"}, "aabcb"); + assertFormat(new Object[] {"a%s%sb", "abc", "def"}, "aabcdefb"); + assertFormat(new Object[] {"a%sc%sb", "abc", "def"}, "aabccdefb"); + assertFormat(new Object[] {"a%s%sb", "abc"}, "aabc%sb"); + assertFormat(new Object[] {"a%sb"}, "a%sb"); + } + + @Test + public void testFormatInt() { + assertFormat(new Object[] {"%d", 100}, "100"); + assertFormat(new Object[] {"%d", -100}, "-100"); + assertFormat(new Object[] {"%d", Integer.MAX_VALUE}, String.valueOf(Integer.MAX_VALUE)); + assertFormat(new Object[] {"%d", Integer.MIN_VALUE}, String.valueOf(Integer.MIN_VALUE)); + assertFormat(new Object[] {"%d", Long.MAX_VALUE}, String.valueOf(Long.MAX_VALUE)); + assertFormat(new Object[] {"%d", Long.MIN_VALUE}, String.valueOf(Long.MIN_VALUE)); + assertFormat(new Object[] {"%d", 100.1D}, "100"); + assertFormat(new Object[] {"%d", -100.7D}, "-100"); + + assertFormat(new Object[] {"%d", Double.NaN}, "NaN"); + assertFormat(new Object[] {"%d", Double.POSITIVE_INFINITY}, "Infinity"); + assertFormat(new Object[] {"%d", Double.NEGATIVE_INFINITY}, "-Infinity"); + assertFormat(new Object[] {"%d", Undefined.instance}, "NaN"); + assertFormat(new Object[] {"%d", SymbolKey.ITERATOR}, "NaN"); + + assertFormat(new Object[] {"%d", 9007199254740991.0D}, "9007199254740991"); + assertFormat(new Object[] {"%d", -9007199254740991.0D}, "-9007199254740991"); + assertFormat(new Object[] {"%d", 9007199254740991L}, "9007199254740991"); + assertFormat(new Object[] {"%d", -9007199254740991L}, "-9007199254740991"); + + assertFormat(new Object[] {"%d", BigInteger.valueOf(100)}, "100n"); + assertFormat(new Object[] {"%d", BigInteger.valueOf(-100)}, "-100n"); + assertFormat( + new Object[] {"%d", new BigInteger("1234567890123456789012345678901234567890")}, + "1234567890123456789012345678901234567890n"); + + assertFormat(new Object[] {"a%d", 100}, "a100"); + assertFormat(new Object[] {"%db", 100}, "100b"); + assertFormat(new Object[] {"a%db", 100}, "a100b"); + assertFormat(new Object[] {"a%d%db", 100, 200}, "a100200b"); + assertFormat(new Object[] {"a%dc%db", 100, 200}, "a100c200b"); + assertFormat(new Object[] {"a%d%db", 100}, "a100%db"); + assertFormat(new Object[] {"a%db"}, "a%db"); + } + + @Test + public void testFormatFloat() { + assertFormat(new Object[] {"%f", 100}, "100"); + assertFormat(new Object[] {"%f", -100}, "-100"); + assertFormat(new Object[] {"%f", Integer.MAX_VALUE}, String.valueOf(Integer.MAX_VALUE)); + assertFormat(new Object[] {"%f", Integer.MIN_VALUE}, String.valueOf(Integer.MIN_VALUE)); + assertFormat(new Object[] {"%f", Long.MAX_VALUE}, "9223372036854776000"); + assertFormat(new Object[] {"%f", Long.MIN_VALUE}, "-9223372036854776000"); + assertFormat(new Object[] {"%f", 100.1D}, "100.1"); + assertFormat(new Object[] {"%f", -100.7D}, "-100.7"); + assertFormat( + new Object[] {"%f", (double) Integer.MAX_VALUE + 0.1D}, + String.valueOf(Integer.MAX_VALUE) + ".1"); + assertFormat( + new Object[] {"%f", (double) Integer.MIN_VALUE - 0.1D}, + String.valueOf(Integer.MIN_VALUE) + ".1"); + + assertFormat(new Object[] {"%f", Double.NaN}, "NaN"); + assertFormat(new Object[] {"%f", Double.POSITIVE_INFINITY}, "Infinity"); + assertFormat(new Object[] {"%f", Double.NEGATIVE_INFINITY}, "-Infinity"); + assertFormat(new Object[] {"%f", Undefined.instance}, "NaN"); + assertFormat(new Object[] {"%f", SymbolKey.ITERATOR}, "NaN"); + + assertFormat(new Object[] {"%f", 9007199254740991.0D}, "9007199254740991"); + assertFormat(new Object[] {"%f", -9007199254740991.0D}, "-9007199254740991"); + assertFormat(new Object[] {"%f", 9007199254740991L}, "9007199254740991"); + assertFormat(new Object[] {"%f", -9007199254740991L}, "-9007199254740991"); + + assertFormat(new Object[] {"%f", BigInteger.valueOf(100)}, "NaN"); + assertFormat(new Object[] {"%f", BigInteger.valueOf(-100)}, "NaN"); + + assertFormat(new Object[] {"a%f", 100}, "a100"); + assertFormat(new Object[] {"%fb", 100}, "100b"); + assertFormat(new Object[] {"a%fb", 100}, "a100b"); + assertFormat(new Object[] {"a%f%fb", 100, 200}, "a100200b"); + assertFormat(new Object[] {"a%fc%fb", 100, 200}, "a100c200b"); + assertFormat(new Object[] {"a%f%fb", 100}, "a100%fb"); + assertFormat(new Object[] {"a%fb"}, "a%fb"); + } + + @Test + public void testFormatObject() { + try (Context cx = Context.enter()) { + Scriptable scope = cx.initStandardObjects(); + + Scriptable emptyObject = cx.newObject(scope); + assertFormat(new Object[] {"%o", emptyObject}, "{}"); + + Scriptable emptyArray = cx.newArray(scope, 0); + assertFormat(new Object[] {"%o", emptyArray}, "[]"); + + Scriptable object1 = cx.newObject(scope); + object1.put("int1", object1, 100); + object1.put("float1", object1, 100.1); + object1.put("string1", object1, "abc"); + assertFormat( + new Object[] {"%o", object1}, + "{" + "\"int1\":100," + "\"float1\":100.1," + "\"string1\":\"abc\"" + "}"); + + Scriptable array1 = cx.newArray(scope, 0); + array1.put(0, array1, 100); + array1.put(1, array1, 100.1); + array1.put(2, array1, "abc"); + assertFormat(new Object[] {"%o", array1}, "[" + "100," + "100.1," + "\"abc\"" + "]"); + + Scriptable object2 = cx.newObject(scope); + object2.put("bigint1", object2, BigInteger.valueOf(100)); + assertFormat(new Object[] {"%o", object2}, "[object Object]"); + } + } + + @Test + public void testPrint() { + assertPrintCalls( + "console.log('abc', 123)", + Collections.singletonList(new PrinterCall(Level.INFO, new Object[] {"abc", 123}))); + + assertPrintCalls( + "console.trace('abc', 123)", + Collections.singletonList( + new PrinterCall( + Level.TRACE, + new Object[] {"abc", 123}, + new ScriptStackElement[] { + new ScriptStackElement("source", null, 1) + }))); + + assertPrintCalls( + "console.debug('abc', 123)", + Collections.singletonList(new PrinterCall(Level.DEBUG, new Object[] {"abc", 123}))); + + assertPrintCalls( + "console.info('abc', 123)", + Collections.singletonList(new PrinterCall(Level.INFO, new Object[] {"abc", 123}))); + + assertPrintCalls( + "console.warn('abc', 123)", + Collections.singletonList(new PrinterCall(Level.WARN, new Object[] {"abc", 123}))); + + assertPrintCalls( + "console.error('abc', 123)", + Collections.singletonList(new PrinterCall(Level.ERROR, new Object[] {"abc", 123}))); + } + + @Test + public void testAssert() { + assertPrintCalls("console.assert(true)", Collections.emptyList()); + + assertPrintCalls( + "console.assert(false)", + Collections.singletonList( + new PrinterCall( + Level.ERROR, new String[] {"Assertion failed: console.assert"}))); + + assertPrintCalls( + "console.assert(false, 'Fail', 1)", + Collections.singletonList( + new PrinterCall(Level.ERROR, new String[] {"Assertion failed: Fail 1"}))); + } + + @Test + public void testCount() { + assertPrintCalls( + "console.count();\n" + + "console.count('a');\n" + + "console.count();\n" + + "console.count('b');\n" + + "console.count('b');\n" + + "console.countReset('b');\n" + + "console.countReset('c');\n" + + "console.count('b');\n" + + "console.count();\n", + Arrays.asList( + new PrinterCall(Level.INFO, new String[] {"default: 1"}), + new PrinterCall(Level.INFO, new String[] {"a: 1"}), + new PrinterCall(Level.INFO, new String[] {"default: 2"}), + new PrinterCall(Level.INFO, new String[] {"b: 1"}), + new PrinterCall(Level.INFO, new String[] {"b: 2"}), + new PrinterCall(Level.WARN, new String[] {"Count for 'c' does not exist."}), + new PrinterCall(Level.INFO, new String[] {"b: 1"}), + new PrinterCall(Level.INFO, new String[] {"default: 3"}))); + } + + @Test + public void testTime() { + assertPrintCalls( + "console.time();\n" + + "console.time('a');\n" + + "console.time();\n" + + "console.time('b');\n" + + "console.timeLog('b');\n" + + "console.timeEnd('b');\n" + + "console.timeLog('b');\n" + + "console.time('b');\n" + + "console.timeLog('b', 'abc', 123);\n" + + "console.timeLog();\n" + + "console.timeEnd('c');\n", + Arrays.asList( + new PrinterCall( + Level.WARN, new Object[] {"Timer 'default' already exists."}), + new PrinterCall(Level.INFO, new Object[] {Pattern.compile("b: [\\d.]+ms")}), + new PrinterCall(Level.INFO, new Object[] {Pattern.compile("b: [\\d.]+ms")}), + new PrinterCall(Level.WARN, new Object[] {"Timer 'b' does not exist."}), + new PrinterCall( + Level.INFO, new Object[] {Pattern.compile("b: [\\d.]+ms abc 123")}), + new PrinterCall( + Level.INFO, new Object[] {Pattern.compile("default: [\\d.]+ms")}), + new PrinterCall(Level.WARN, new Object[] {"Timer 'c' does not exist."}))); + } + + private void assertFormat(Object[] args, String expected) { + try (Context cx = Context.enter()) { + Scriptable scope = cx.initStandardObjects(); + assertEquals(expected, NativeConsole.format(cx, scope, args)); + } + } + + private void assertPrintCalls(String source, List expectedCalls) { + DummyConsolePrinter printer = new DummyConsolePrinter(); + + try (Context cx = Context.enter()) { + Scriptable scope = cx.initStandardObjects(); + NativeConsole.init(scope, false, printer); + cx.evaluateString(scope, source, "source", 1, null); + printer.assertCalls(expectedCalls); + } + } + + private void assertException(Object[] args, Class ex) { + try (Context cx = Context.enter()) { + Scriptable scope = cx.initStandardObjects(); + NativeConsole.format(cx, scope, args); + fail(); + } catch (Exception e) { + if (!ex.isInstance(e)) { + fail(); + } + } + } +} diff --git a/toolsrc/org/mozilla/javascript/tools/shell/Global.java b/toolsrc/org/mozilla/javascript/tools/shell/Global.java index f8835f0e6f..87d0a4f1a9 100644 --- a/toolsrc/org/mozilla/javascript/tools/shell/Global.java +++ b/toolsrc/org/mozilla/javascript/tools/shell/Global.java @@ -38,6 +38,7 @@ import org.mozilla.javascript.Function; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.NativeArray; +import org.mozilla.javascript.NativeConsole; import org.mozilla.javascript.RhinoException; import org.mozilla.javascript.Script; import org.mozilla.javascript.ScriptRuntime; @@ -105,6 +106,7 @@ public void init(Context cx) { // Define some global functions particular to the shell. Note // that these functions are not part of ECMA. initStandardObjects(cx, sealedStdLib); + NativeConsole.init(this, sealedStdLib, new ShellConsolePrinter()); String[] names = { "defineClass", "deserialize", diff --git a/toolsrc/org/mozilla/javascript/tools/shell/ShellConsolePrinter.java b/toolsrc/org/mozilla/javascript/tools/shell/ShellConsolePrinter.java new file mode 100644 index 0000000000..267370d1fc --- /dev/null +++ b/toolsrc/org/mozilla/javascript/tools/shell/ShellConsolePrinter.java @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.tools.shell; + +import java.io.IOException; +import java.nio.charset.Charset; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.NativeConsole; +import org.mozilla.javascript.ScriptStackElement; +import org.mozilla.javascript.Scriptable; + +/** Provide a printer use in console API */ +class ShellConsolePrinter implements NativeConsole.ConsolePrinter { + private static final long serialVersionUID = 5869832740127501857L; + + @Override + public void print( + Context cx, + Scriptable scope, + NativeConsole.Level level, + Object[] args, + ScriptStackElement[] stack) { + if (args.length == 0) { + return; + } + + String msg = NativeConsole.format(cx, scope, args); + ShellConsole console = Main.getGlobal().getConsole(Charset.defaultCharset()); + try { + console.println(level + " " + msg); + + if (stack != null) { + for (ScriptStackElement element : stack) { + console.println(element.toString()); + } + } + } catch (IOException e) { + throw Context.reportRuntimeError(e.getMessage()); + } + } +}