From 099cb420b9d20e2822bf1c3812e4c5fb4a942a2e Mon Sep 17 00:00:00 2001 From: Benedikt Meurer Date: Wed, 22 Dec 2021 12:25:21 +0100 Subject: [PATCH] [console] Proper type conversions in console builtins. This updates the following set of console builtins in V8 to match the Console Standard (https://console.spec.whatwg.org) with respect to (potentially side effecting) type conversions: - console.debug - console.error - console.info - console.log - console.trace - console.warn - console.group - console.groupCollapsed - console.assert The V8 implementation only performs the type conversions and updates the arguments in-place with the results from the %String% constructor, %parseInt%, or %parseFloat% invocations. The actual formatting is still left completely to the debugger front-end. To give a concrete example, the following code ```js const msgFmt = { toString() { return 'Message %i' } }; console.log('LOG: %s`, msgFmt, 42); ``` sends the following parameters to the debugger front-end ```js ["LOG: %s", "Message %i", 42] ``` and it's then the job of the front-end to perform the actual string substitutions. It's also worth calling out that the console builtins are only concerned with %s, %f, %d, and %i formatting specifiers, since these are the only ones that trigger type conversions, and %o, %O, and %c can only be implemented in a meaningful way at a higher level. Fixed: chromium:1277944 Bug: chromium:1282076 Doc: https://bit.ly/v8-proper-console-type-conversions Spec: https://console.spec.whatwg.org Change-Id: I0996680811aa96236bd0d879e4a11101629ef1a7 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3352118 Reviewed-by: Kim-Anh Tran Auto-Submit: Benedikt Meurer Reviewed-by: Igor Sheludko Commit-Queue: Igor Sheludko Cr-Commit-Position: refs/heads/main@{#78432} --- src/builtins/builtins-console.cc | 156 +++- src/init/bootstrapper.cc | 2 + src/objects/contexts.h | 2 + .../runtime/console-context-expected.txt | 38 +- test/inspector/runtime/console-context.js | 9 +- .../runtime/console-formatter-expected.txt | 700 ++++++++++++++++++ test/inspector/runtime/console-formatter.js | 144 ++++ 7 files changed, 1010 insertions(+), 41 deletions(-) create mode 100644 test/inspector/runtime/console-formatter-expected.txt create mode 100644 test/inspector/runtime/console-formatter.js diff --git a/src/builtins/builtins-console.cc b/src/builtins/builtins-console.cc index a1359cd422..35528cc0e8 100644 --- a/src/builtins/builtins-console.cc +++ b/src/builtins/builtins-console.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include + #include "src/api/api-inl.h" #include "src/builtins/builtins-utils-inl.h" #include "src/builtins/builtins.h" @@ -16,28 +18,128 @@ namespace internal { // ----------------------------------------------------------------------------- // Console -#define CONSOLE_METHOD_LIST(V) \ - V(Debug, debug) \ - V(Error, error) \ - V(Info, info) \ - V(Log, log) \ - V(Warn, warn) \ - V(Dir, dir) \ - V(DirXml, dirXml) \ - V(Table, table) \ - V(Trace, trace) \ - V(Group, group) \ - V(GroupCollapsed, groupCollapsed) \ - V(GroupEnd, groupEnd) \ - V(Clear, clear) \ - V(Count, count) \ - V(CountReset, countReset) \ - V(Assert, assert) \ - V(Profile, profile) \ - V(ProfileEnd, profileEnd) \ +#define CONSOLE_METHOD_LIST(V) \ + V(Dir, dir) \ + V(DirXml, dirXml) \ + V(Table, table) \ + V(GroupEnd, groupEnd) \ + V(Clear, clear) \ + V(Count, count) \ + V(CountReset, countReset) \ + V(Profile, profile) \ + V(ProfileEnd, profileEnd) \ V(TimeLog, timeLog) +#define CONSOLE_METHOD_WITH_FORMATTER_LIST(V) \ + V(Debug, debug, 1) \ + V(Error, error, 1) \ + V(Info, info, 1) \ + V(Log, log, 1) \ + V(Warn, warn, 1) \ + V(Trace, trace, 1) \ + V(Group, group, 1) \ + V(GroupCollapsed, groupCollapsed, 1) \ + V(Assert, assert, 2) + namespace { + +// 2.2 Formatter(args) [https://console.spec.whatwg.org/#formatter] +// +// This implements the formatter operation defined in the Console +// specification to the degree that it makes sense for V8. That +// means we primarily deal with %s, %i, %f, and %d, and any side +// effects caused by the type conversions, and we preserve the %o, +// %c, and %O specifiers and their parameters unchanged, and instead +// leave it to the debugger front-end to make sense of those. +// +// Chrome also supports the non-standard bypass format specifier %_ +// which just skips over the parameter. +// +// This implementation updates the |args| in-place with the results +// from the conversion. +// +// The |index| describes the position of the format string within, +// |args| (starting with 1, since |args| also includes the receiver), +// which is different for example in case of `console.log` where it +// is 1 compared to `console.assert` where it is 2. +bool Formatter(Isolate* isolate, BuiltinArguments& args, int index) { + if (args.length() < index + 2 || !args[index].IsString()) { + return true; + } + struct State { + Handle str; + int off; + }; + std::stack states; + HandleScope scope(isolate); + auto percent = isolate->factory()->LookupSingleCharacterStringFromCode('%'); + states.push({args.at(index++), 0}); + while (!states.empty() && index < args.length()) { + State& state = states.top(); + state.off = String::IndexOf(isolate, state.str, percent, state.off); + if (state.off < 0 || state.off == state.str->length() - 1) { + states.pop(); + continue; + } + Handle current = args.at(index); + uint16_t specifier = state.str->Get(state.off + 1, isolate); + if (specifier == 'd' || specifier == 'f' || specifier == 'i') { + if (current->IsSymbol()) { + current = isolate->factory()->nan_value(); + } else { + Handle params[] = {current, + isolate->factory()->NewNumberFromInt(10)}; + auto builtin = specifier == 'f' ? isolate->global_parse_float_fun() + : isolate->global_parse_int_fun(); + if (!Execution::CallBuiltin(isolate, builtin, + isolate->factory()->undefined_value(), + arraysize(params), params) + .ToHandle(¤t)) { + return false; + } + } + } else if (specifier == 's') { + Handle params[] = {current}; + if (!Execution::CallBuiltin(isolate, isolate->string_function(), + isolate->factory()->undefined_value(), + arraysize(params), params) + .ToHandle(¤t)) { + return false; + } + + // Recurse into string results from type conversions, as they + // can themselves contain formatting specifiers. + states.push({Handle::cast(current), 0}); + } else if (specifier == 'c' || specifier == 'o' || specifier == 'O' || + specifier == '_') { + // We leave the interpretation of %c (CSS), %o (optimally useful + // formatting), and %O (generic JavaScript object formatting) as + // well as the non-standard %_ (bypass formatter in Chrome) to + // the debugger front-end, and preserve these specifiers as well + // as their arguments verbatim. + index++; + state.off += 2; + continue; + } else if (specifier == '%') { + // Chrome also supports %% as a way to generate a single % in the + // output. + state.off += 2; + continue; + } else { + state.off++; + continue; + } + + // Replace the |specifier| (including the '%' character) in |target| + // with the |current| value. We perform the replacement only morally + // by updating the argument to the conversion result, but leave it to + // the debugger front-end to perform the actual substitution. + args.set_at(index++, *current); + state.off += 2; + } + return true; +} + void ConsoleCall( Isolate* isolate, const internal::BuiltinArguments& args, void (debug::ConsoleDelegate::*func)(const v8::debug::ConsoleCallArguments&, @@ -74,6 +176,7 @@ void LogTimerEvent(Isolate* isolate, BuiltinArguments args, } LOG(isolate, TimerEvent(se, raw_name)); } + } // namespace #define CONSOLE_BUILTIN_IMPLEMENTATION(call, name) \ @@ -85,6 +188,18 @@ void LogTimerEvent(Isolate* isolate, BuiltinArguments args, CONSOLE_METHOD_LIST(CONSOLE_BUILTIN_IMPLEMENTATION) #undef CONSOLE_BUILTIN_IMPLEMENTATION +#define CONSOLE_BUILTIN_IMPLEMENTATION(call, name, index) \ + BUILTIN(Console##call) { \ + if (!Formatter(isolate, args, index)) { \ + return ReadOnlyRoots(isolate).exception(); \ + } \ + ConsoleCall(isolate, args, &debug::ConsoleDelegate::call); \ + RETURN_FAILURE_IF_SCHEDULED_EXCEPTION(isolate); \ + return ReadOnlyRoots(isolate).undefined_value(); \ + } +CONSOLE_METHOD_WITH_FORMATTER_LIST(CONSOLE_BUILTIN_IMPLEMENTATION) +#undef CONSOLE_BUILTIN_IMPLEMENTATION + BUILTIN(ConsoleTime) { LogTimerEvent(isolate, args, v8::LogEventStatus::kStart); ConsoleCall(isolate, args, &debug::ConsoleDelegate::Time); @@ -162,10 +277,11 @@ BUILTIN(ConsoleContext) { int id = isolate->last_console_context_id() + 1; isolate->set_last_console_context_id(id); -#define CONSOLE_BUILTIN_SETUP(call, name) \ +#define CONSOLE_BUILTIN_SETUP(call, name, ...) \ InstallContextFunction(isolate, context, #name, Builtin::kConsole##call, id, \ args.at(1)); CONSOLE_METHOD_LIST(CONSOLE_BUILTIN_SETUP) + CONSOLE_METHOD_WITH_FORMATTER_LIST(CONSOLE_BUILTIN_SETUP) #undef CONSOLE_BUILTIN_SETUP InstallContextFunction(isolate, context, "time", Builtin::kConsoleTime, id, args.at(1)); diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc index 1601543507..39f6a10ef6 100644 --- a/src/init/bootstrapper.cc +++ b/src/init/bootstrapper.cc @@ -1901,12 +1901,14 @@ void Genesis::InitializeGlobal(Handle global_object, Builtin::kNumberParseFloat, 1, true); JSObject::AddProperty(isolate_, global_object, "parseFloat", parse_float_fun, DONT_ENUM); + native_context()->set_global_parse_float_fun(*parse_float_fun); // Install Number.parseInt and Global.parseInt. Handle parse_int_fun = SimpleInstallFunction( isolate_, number_fun, "parseInt", Builtin::kNumberParseInt, 2, true); JSObject::AddProperty(isolate_, global_object, "parseInt", parse_int_fun, DONT_ENUM); + native_context()->set_global_parse_int_fun(*parse_int_fun); // Install Number constants const double kMaxValue = 1.7976931348623157e+308; diff --git a/src/objects/contexts.h b/src/objects/contexts.h index 009aa7cd0b..2bf9199ccc 100644 --- a/src/objects/contexts.h +++ b/src/objects/contexts.h @@ -325,6 +325,8 @@ enum ContextLookupFlags { V(EVAL_ERROR_FUNCTION_INDEX, JSFunction, eval_error_function) \ V(AGGREGATE_ERROR_FUNCTION_INDEX, JSFunction, aggregate_error_function) \ V(GLOBAL_EVAL_FUN_INDEX, JSFunction, global_eval_fun) \ + V(GLOBAL_PARSE_FLOAT_FUN_INDEX, JSFunction, global_parse_float_fun) \ + V(GLOBAL_PARSE_INT_FUN_INDEX, JSFunction, global_parse_int_fun) \ V(GLOBAL_PROXY_FUNCTION_INDEX, JSFunction, global_proxy_function) \ V(MAP_DELETE_INDEX, JSFunction, map_delete) \ V(MAP_GET_INDEX, JSFunction, map_get) \ diff --git a/test/inspector/runtime/console-context-expected.txt b/test/inspector/runtime/console-context-expected.txt index 658238aaa2..8a66dca15e 100644 --- a/test/inspector/runtime/console-context-expected.txt +++ b/test/inspector/runtime/console-context-expected.txt @@ -10,28 +10,28 @@ console.context description: } console.context() methods: [ - [0] : debug - [1] : error - [2] : info - [3] : log - [4] : warn + [0] : assert + [1] : clear + [2] : count + [3] : countReset + [4] : debug [5] : dir [6] : dirXml - [7] : table - [8] : trace - [9] : group - [10] : groupCollapsed - [11] : groupEnd - [12] : clear - [13] : count - [14] : countReset - [15] : assert - [16] : profile - [17] : profileEnd + [7] : error + [8] : group + [9] : groupCollapsed + [10] : groupEnd + [11] : info + [12] : log + [13] : profile + [14] : profileEnd + [15] : table + [16] : time + [17] : timeEnd [18] : timeLog - [19] : time - [20] : timeEnd - [21] : timeStamp + [19] : timeStamp + [20] : trace + [21] : warn ] Running test: testDefaultConsoleContext diff --git a/test/inspector/runtime/console-context.js b/test/inspector/runtime/console-context.js index 74996ae595..6d076357c5 100644 --- a/test/inspector/runtime/console-context.js +++ b/test/inspector/runtime/console-context.js @@ -11,9 +11,14 @@ InspectorTest.runAsyncTestSuite([ expression: 'console.context'}); InspectorTest.logMessage(result); + // Enumerate the methods alpha-sorted to make the test + // independent of the (unspecified) enumeration order + // of console.context() methods. InspectorTest.log('console.context() methods:'); - var {result:{result:{value}}} = await Protocol.Runtime.evaluate({ - expression: 'Object.keys(console.context())', returnByValue: true}); + var {result: {result: {value}}} = await Protocol.Runtime.evaluate({ + expression: 'Object.keys(console.context()).sort()', + returnByValue: true + }); InspectorTest.logMessage(value); }, diff --git a/test/inspector/runtime/console-formatter-expected.txt b/test/inspector/runtime/console-formatter-expected.txt new file mode 100644 index 0000000000..02a25d5a0d --- /dev/null +++ b/test/inspector/runtime/console-formatter-expected.txt @@ -0,0 +1,700 @@ +Test for console formatting + +Running test: testFloatFormatter +Testing console.debug('%f', 3.1415)... +debug[ + [0] : { + type : string + value : %f + } + [1] : { + description : 3.1415 + type : number + value : 3.1415 + } +] +Testing console.error('%f', '3e2')... +error[ + [0] : { + type : string + value : %f + } + [1] : { + description : 300 + type : number + value : 300 + } +] +Testing console.info('%f', Symbol('1.1'))... +info[ + [0] : { + type : string + value : %f + } + [1] : { + description : NaN + type : number + unserializableValue : NaN + } +] +Testing console.log('%f', {toString() { return '42'; }})... +log[ + [0] : { + type : string + value : %f + } + [1] : { + description : 42 + type : number + value : 42 + } +] +Testing console.trace('%f', {[Symbol.toPrimitive]() { return 2.78; }})... +trace[ + [0] : { + type : string + value : %f + } + [1] : { + description : 2.78 + type : number + value : 2.78 + } +] +Testing console.warn('%f', {toString() { throw new Error(); }})... +{ + columnNumber : 33 + exception : { + className : Error + description : Error at Object.toString (:1:40) at parseFloat () at console.warn () at :1:9 + objectId : + subtype : error + type : object + } + exceptionId : + lineNumber : 0 + scriptId : + stackTrace : { + callFrames : [ + [0] : { + columnNumber : 39 + functionName : toString + lineNumber : 0 + scriptId : + url : + } + [1] : { + columnNumber : 8 + functionName : + lineNumber : 0 + scriptId : + url : + } + ] + } + text : Uncaught +} + +Running test: testIntegerFormatter +Testing console.debug('%d', 42)... +debug[ + [0] : { + type : string + value : %d + } + [1] : { + description : 42 + type : number + value : 42 + } +] +Testing console.error('%i', '987654321')... +error[ + [0] : { + type : string + value : %i + } + [1] : { + description : 987654321 + type : number + value : 987654321 + } +] +Testing console.info('%d', Symbol('12345'))... +info[ + [0] : { + type : string + value : %d + } + [1] : { + description : NaN + type : number + unserializableValue : NaN + } +] +Testing console.log('%i', {toString() { return '42'; }})... +log[ + [0] : { + type : string + value : %i + } + [1] : { + description : 42 + type : number + value : 42 + } +] +Testing console.trace('%d', {[Symbol.toPrimitive]() { return 256; }})... +trace[ + [0] : { + type : string + value : %d + } + [1] : { + description : 256 + type : number + value : 256 + } +] +Testing console.warn('%i', {toString() { throw new Error(); }})... +{ + columnNumber : 33 + exception : { + className : Error + description : Error at Object.toString (:1:40) at parseInt () at console.warn () at :1:9 + objectId : + subtype : error + type : object + } + exceptionId : + lineNumber : 0 + scriptId : + stackTrace : { + callFrames : [ + [0] : { + columnNumber : 39 + functionName : toString + lineNumber : 0 + scriptId : + url : + } + [1] : { + columnNumber : 8 + functionName : + lineNumber : 0 + scriptId : + url : + } + ] + } + text : Uncaught +} + +Running test: testStringFormatter +Testing console.debug('%s', 42)... +debug[ + [0] : { + type : string + value : %s + } + [1] : { + type : string + value : 42 + } +] +Testing console.error('%s', 'Test string')... +error[ + [0] : { + type : string + value : %s + } + [1] : { + type : string + value : Test string + } +] +Testing console.info('%s', Symbol('Test symbol'))... +info[ + [0] : { + type : string + value : %s + } + [1] : { + type : string + value : Symbol(Test symbol) + } +] +Testing console.log('%s', {toString() { return 'Test object'; }})... +log[ + [0] : { + type : string + value : %s + } + [1] : { + type : string + value : Test object + } +] +Testing console.trace('%s', {[Symbol.toPrimitive]() { return true; }})... +trace[ + [0] : { + type : string + value : %s + } + [1] : { + type : string + value : true + } +] +Testing console.warn('%s', {toString() { throw new Error(); }})... +{ + columnNumber : 33 + exception : { + className : Error + description : Error at Object.toString (:1:40) at String () at console.warn () at :1:9 + objectId : + subtype : error + type : object + } + exceptionId : + lineNumber : 0 + scriptId : + stackTrace : { + callFrames : [ + [0] : { + columnNumber : 39 + functionName : toString + lineNumber : 0 + scriptId : + url : + } + [1] : { + columnNumber : 8 + functionName : + lineNumber : 0 + scriptId : + url : + } + ] + } + text : Uncaught +} + +Running test: testOtherFormatters +Testing console.debug('%c', 'color:red')... +debug[ + [0] : { + type : string + value : %c + } + [1] : { + type : string + value : color:red + } +] +Testing console.error('%o', {toString() { throw new Error(); }})... +error[ + [0] : { + type : string + value : %o + } + [1] : { + className : Object + description : Object + objectId : 1.1.7 + preview : { + description : Object + overflow : false + properties : [ + [0] : { + name : toString + type : function + value : + } + ] + type : object + } + type : object + } +] +Testing console.info('%O', {toString() { throw new Error(); }})... +info[ + [0] : { + type : string + value : %O + } + [1] : { + className : Object + description : Object + objectId : 1.1.8 + preview : { + description : Object + overflow : false + properties : [ + [0] : { + name : toString + type : function + value : + } + ] + type : object + } + type : object + } +] +Testing console.log('We have reached 100% of our users', 'with this!')... +log[ + [0] : { + type : string + value : We have reached 100% of our users + } + [1] : { + type : string + value : with this! + } +] + +Running test: testMultipleFormatters +Testing console.debug('%s%some Text%i', '', 'S', 1)... +debug[ + [0] : { + type : string + value : %s%some Text%i + } + [1] : { + type : string + value : + } + [2] : { + type : string + value : S + } + [3] : { + description : 1 + type : number + value : 1 + } +] +Testing console.error('%c%i%c%s', 'color:red', 42, 'color:green', 'Message!')... +error[ + [0] : { + type : string + value : %c%i%c%s + } + [1] : { + type : string + value : color:red + } + [2] : { + description : 42 + type : number + value : 42 + } + [3] : { + type : string + value : color:green + } + [4] : { + type : string + value : Message! + } +] +Testing console.info('%s', {toString() { return '%i% %s %s'; }}, {toString() { return '100'; }}, 'more', 'arguments')... +info[ + [0] : { + type : string + value : %s + } + [1] : { + type : string + value : %i% %s %s + } + [2] : { + description : 100 + type : number + value : 100 + } + [3] : { + type : string + value : more + } + [4] : { + type : string + value : arguments + } +] +Testing console.log('%s %s', {toString() { return 'Too %s %s'; }}, 'many', 'specifiers')... +log[ + [0] : { + type : string + value : %s %s + } + [1] : { + type : string + value : Too %s %s + } + [2] : { + type : string + value : many + } + [3] : { + type : string + value : specifiers + } +] +Testing console.trace('%s %f', {toString() { return '%s'; }}, {[Symbol.toPrimitive]() { return 'foo'; }}, 1, 'Test')... +trace[ + [0] : { + type : string + value : %s %f + } + [1] : { + type : string + value : %s + } + [2] : { + type : string + value : foo + } + [3] : { + description : 1 + type : number + value : 1 + } + [4] : { + type : string + value : Test + } +] + +Running test: testAssert +Testing console.assert(true, '%s', {toString() { throw new Error(); }})... +Testing console.assert(false, '%s %i', {toString() { return '%s'; }}, {[Symbol.toPrimitive]() { return 1; }}, 1, 'Test')... +assert[ + [0] : { + type : string + value : %s %i + } + [1] : { + type : string + value : %s + } + [2] : { + type : string + value : 1 + } + [3] : { + description : 1 + type : number + value : 1 + } + [4] : { + type : string + value : Test + } +] +Testing console.assert(false, '%s', {toString() { throw new Error(); }})... +{ + columnNumber : 42 + exception : { + className : Error + description : Error at Object.toString (:1:49) at String () at console.assert () at :1:9 + objectId : + subtype : error + type : object + } + exceptionId : + lineNumber : 0 + scriptId : + stackTrace : { + callFrames : [ + [0] : { + columnNumber : 48 + functionName : toString + lineNumber : 0 + scriptId : + url : + } + [1] : { + columnNumber : 8 + functionName : + lineNumber : 0 + scriptId : + url : + } + ] + } + text : Uncaught +} + +Running test: testGroup +Testing console.group('%s', {toString() { throw new Error(); }})... +{ + columnNumber : 34 + exception : { + className : Error + description : Error at Object.toString (:1:41) at String () at console.group () at :1:9 + objectId : + subtype : error + type : object + } + exceptionId : + lineNumber : 0 + scriptId : + stackTrace : { + callFrames : [ + [0] : { + columnNumber : 40 + functionName : toString + lineNumber : 0 + scriptId : + url : + } + [1] : { + columnNumber : 8 + functionName : + lineNumber : 0 + scriptId : + url : + } + ] + } + text : Uncaught +} +Testing console.group('%s%i', 'Gruppe', {[Symbol.toPrimitive]() { return 1; }})... +startGroup[ + [0] : { + type : string + value : %s%i + } + [1] : { + type : string + value : Gruppe + } + [2] : { + description : 1 + type : number + value : 1 + } +] +Testing console.groupEnd()... +endGroup[ + [0] : { + type : string + value : console.groupEnd + } +] + +Running test: testGroupCollapsed +Testing console.groupCollapsed('%d', {toString() { throw new Error(); }})... +{ + columnNumber : 43 + exception : { + className : Error + description : Error at Object.toString (:1:50) at parseInt () at console.groupCollapsed () at :1:9 + objectId : + subtype : error + type : object + } + exceptionId : + lineNumber : 0 + scriptId : + stackTrace : { + callFrames : [ + [0] : { + columnNumber : 49 + functionName : toString + lineNumber : 0 + scriptId : + url : + } + [1] : { + columnNumber : 8 + functionName : + lineNumber : 0 + scriptId : + url : + } + ] + } + text : Uncaught +} +Testing console.groupCollapsed('%s%f', {[Symbol.toPrimitive]() { return 'Gruppe'; }}, 3.1415)... +startGroupCollapsed[ + [0] : { + type : string + value : %s%f + } + [1] : { + type : string + value : Gruppe + } + [2] : { + description : 3.1415 + type : number + value : 3.1415 + } +] +Testing console.groupEnd()... +endGroup[ + [0] : { + type : string + value : console.groupEnd + } +] + +Running test: testNonStandardFormatSpecifiers +Testing console.log('%_ %s', {toString() { throw new Error(); }}, {toString() { return 'foo'; }})... +log[ + [0] : { + type : string + value : %_ %s + } + [1] : { + className : Object + description : Object + objectId : 1.1.15 + preview : { + description : Object + overflow : false + properties : [ + [0] : { + name : toString + type : function + value : + } + ] + type : object + } + type : object + } + [2] : { + type : string + value : foo + } +] +Testing console.log('%%s', {toString() { throw new Error(); }})... +log[ + [0] : { + type : string + value : %%s + } + [1] : { + className : Object + description : Object + objectId : 1.1.16 + preview : { + description : Object + overflow : false + properties : [ + [0] : { + name : toString + type : function + value : + } + ] + type : object + } + type : object + } +] diff --git a/test/inspector/runtime/console-formatter.js b/test/inspector/runtime/console-formatter.js new file mode 100644 index 0000000000..34d4b4a368 --- /dev/null +++ b/test/inspector/runtime/console-formatter.js @@ -0,0 +1,144 @@ +// Copyright 2021 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const {session, contextGroup, Protocol} = + InspectorTest.start('Test for console formatting'); + +Protocol.Runtime.onConsoleAPICalled(({params: {args, type}}) => { + InspectorTest.logObject(args, type); +}); + +async function test(expression) { + InspectorTest.logMessage(`Testing ${expression}...`); + const {result} = await Protocol.Runtime.evaluate({expression}); + if ('exceptionDetails' in result) { + InspectorTest.logMessage(result.exceptionDetails); + } +} + +InspectorTest.runAsyncTestSuite([ + async function testFloatFormatter() { + await Protocol.Runtime.enable(); + await test(`console.debug('%f', 3.1415)`); + await test(`console.error('%f', '3e2')`); + await test(`console.info('%f', Symbol('1.1'))`); + await test(`console.log('%f', {toString() { return '42'; }})`); + await test( + `console.trace('%f', {[Symbol.toPrimitive]() { return 2.78; }})`); + await test(`console.warn('%f', {toString() { throw new Error(); }})`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testIntegerFormatter() { + await Protocol.Runtime.enable(); + await test(`console.debug('%d', 42)`); + await test(`console.error('%i', '987654321')`); + await test(`console.info('%d', Symbol('12345'))`); + await test(`console.log('%i', {toString() { return '42'; }})`); + await test(`console.trace('%d', {[Symbol.toPrimitive]() { return 256; }})`); + await test(`console.warn('%i', {toString() { throw new Error(); }})`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testStringFormatter() { + await Protocol.Runtime.enable(); + await test(`console.debug('%s', 42)`); + await test(`console.error('%s', 'Test string')`); + await test(`console.info('%s', Symbol('Test symbol'))`); + await test(`console.log('%s', {toString() { return 'Test object'; }})`); + await test( + `console.trace('%s', {[Symbol.toPrimitive]() { return true; }})`); + await test(`console.warn('%s', {toString() { throw new Error(); }})`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testOtherFormatters() { + await Protocol.Runtime.enable(); + await test(`console.debug('%c', 'color:red')`); + await test(`console.error('%o', {toString() { throw new Error(); }})`); + await test(`console.info('%O', {toString() { throw new Error(); }})`); + await test( + `console.log('We have reached 100% of our users', 'with this!')`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testMultipleFormatters() { + await Protocol.Runtime.enable(); + await test(`console.debug('%s%some Text%i', '', 'S', 1)`); + await test( + `console.error('%c%i%c%s', 'color:red', 42, 'color:green', 'Message!')`); + await test( + `console.info('%s', {toString() { return '%i% %s %s'; }}, {toString() { return '100'; }}, 'more', 'arguments')`); + await test( + `console.log('%s %s', {toString() { return 'Too %s %s'; }}, 'many', 'specifiers')`); + await test( + `console.trace('%s %f', {toString() { return '%s'; }}, {[Symbol.toPrimitive]() { return 'foo'; }}, 1, 'Test')`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testAssert() { + await Protocol.Runtime.enable(); + await test( + `console.assert(true, '%s', {toString() { throw new Error(); }})`); + await test( + `console.assert(false, '%s %i', {toString() { return '%s'; }}, {[Symbol.toPrimitive]() { return 1; }}, 1, 'Test')`); + await test( + `console.assert(false, '%s', {toString() { throw new Error(); }})`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testGroup() { + await Protocol.Runtime.enable(); + await test(`console.group('%s', {toString() { throw new Error(); }})`); + await test( + `console.group('%s%i', 'Gruppe', {[Symbol.toPrimitive]() { return 1; }})`); + await test(`console.groupEnd()`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testGroupCollapsed() { + await Protocol.Runtime.enable(); + await test( + `console.groupCollapsed('%d', {toString() { throw new Error(); }})`); + await test( + `console.groupCollapsed('%s%f', {[Symbol.toPrimitive]() { return 'Gruppe'; }}, 3.1415)`); + await test(`console.groupEnd()`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + }, + + async function testNonStandardFormatSpecifiers() { + await Protocol.Runtime.enable(); + await test( + `console.log('%_ %s', {toString() { throw new Error(); }}, {toString() { return 'foo'; }})`); + await test(`console.log('%%s', {toString() { throw new Error(); }})`); + await Promise.all([ + Protocol.Runtime.discardConsoleEntries(), + Protocol.Runtime.disable(), + ]); + } +]);