From 30fdf6dc83d3a9d44436528959c39d3eab14cbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sabiniarz?= <31597105+mhvsa@users.noreply.github.com> Date: Tue, 31 Mar 2020 01:01:19 +0200 Subject: [PATCH] console: print promise details (#4524) --- cli/js/globals.ts | 13 ++++++ cli/js/tests/console_test.ts | 21 ++++++++++ cli/js/web/console.ts | 34 +++++++++++++++- cli/js/web/promise.ts | 7 ++++ core/bindings.rs | 79 ++++++++++++++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 cli/js/web/promise.ts diff --git a/cli/js/globals.ts b/cli/js/globals.ts index 87ff13b22616c7..e5b7ff85a05623 100644 --- a/cli/js/globals.ts +++ b/cli/js/globals.ts @@ -2,6 +2,7 @@ import * as blob from "./web/blob.ts"; import * as consoleTypes from "./web/console.ts"; +import * as promiseTypes from "./web/promise.ts"; import * as customEvent from "./web/custom_event.ts"; import * as domTypes from "./web/dom_types.ts"; import * as domFile from "./web/dom_file.ts"; @@ -102,6 +103,18 @@ declare global { formatError: (e: Error) => string; + /** + * Get promise details as two elements array. + * + * First element is the `PromiseState`. + * If promise isn't pending, second element would be the result of the promise. + * Otherwise, second element would be undefined. + * + * Throws `TypeError` if argument isn't a promise + * + */ + getPromiseDetails(promise: Promise): promiseTypes.PromiseDetails; + decode(bytes: Uint8Array): string; encode(text: string): Uint8Array; } diff --git a/cli/js/tests/console_test.ts b/cli/js/tests/console_test.ts index c49c941f401b84..88902f84be696f 100644 --- a/cli/js/tests/console_test.ts +++ b/cli/js/tests/console_test.ts @@ -580,6 +580,27 @@ unitTest(function consoleTestStringifyIterable() { ); }); +unitTest(async function consoleTestStringifyPromises(): Promise { + const pendingPromise = new Promise((_res, _rej) => {}); + assertEquals(stringify(pendingPromise), "Promise { }"); + + const resolvedPromise = new Promise((res, _rej) => { + res("Resolved!"); + }); + assertEquals(stringify(resolvedPromise), `Promise { "Resolved!" }`); + + let rejectedPromise; + try { + rejectedPromise = new Promise((_, rej) => { + rej(Error("Whoops")); + }); + await rejectedPromise; + } catch (err) {} + const strLines = stringify(rejectedPromise).split("\n"); + assertEquals(strLines[0], "Promise {"); + assertEquals(strLines[1], " Error: Whoops"); +}); + unitTest(function consoleTestWithCustomInspector(): void { class A { [customInspect](): string { diff --git a/cli/js/web/console.ts b/cli/js/web/console.ts index a9b4d53be7ef95..3a274e0865785d 100644 --- a/cli/js/web/console.ts +++ b/cli/js/web/console.ts @@ -4,6 +4,7 @@ import { TextEncoder } from "./text_encoding.ts"; import { File, stdout } from "../files.ts"; import { cliTable } from "./console_table.ts"; import { exposeForTest } from "../internals.ts"; +import { PromiseState } from "./promise.ts"; type ConsoleContext = Set; type InspectOptions = Partial<{ @@ -28,6 +29,8 @@ const CHAR_LOWERCASE_O = 111; /* o */ const CHAR_UPPERCASE_O = 79; /* O */ const CHAR_LOWERCASE_C = 99; /* c */ +const PROMISE_STRING_BASE_LENGTH = 12; + export class CSI { static kClear = "\x1b[1;1H"; static kClearScreenDown = "\x1b[0J"; @@ -442,7 +445,34 @@ function createNumberWrapperString(value: Number): string { /* eslint-enable @typescript-eslint/ban-types */ -// TODO: Promise, requires v8 bindings to get info +function createPromiseString( + value: Promise, + ctx: ConsoleContext, + level: number, + maxLevel: number +): string { + const [state, result] = Deno.core.getPromiseDetails(value); + + if (state === PromiseState.Pending) { + return "Promise { }"; + } + + const prefix = state === PromiseState.Fulfilled ? "" : " "; + + const str = `${prefix}${stringifyWithQuotes( + result, + ctx, + level + 1, + maxLevel + )}`; + + if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) { + return `Promise {\n${" ".repeat(level + 1)}${str}\n}`; + } + + return `Promise { ${str} }`; +} + // TODO: Proxy function createRawObjectString( @@ -531,6 +561,8 @@ function createObjectString( return createBooleanWrapperString(value); } else if (value instanceof String) { return createStringWrapperString(value); + } else if (value instanceof Promise) { + return createPromiseString(value, ...args); } else if (value instanceof RegExp) { return createRegExpString(value); } else if (value instanceof Date) { diff --git a/cli/js/web/promise.ts b/cli/js/web/promise.ts new file mode 100644 index 00000000000000..b00c0786fe5fec --- /dev/null +++ b/cli/js/web/promise.ts @@ -0,0 +1,7 @@ +export enum PromiseState { + Pending = 0, + Fulfilled = 1, + Rejected = 2, +} + +export type PromiseDetails = [PromiseState, T | undefined]; diff --git a/core/bindings.rs b/core/bindings.rs index 88bdf7f304ffd1..c89c6842480f1c 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -45,6 +45,9 @@ lazy_static! { v8::ExternalReference { function: decode.map_fn_to() }, + v8::ExternalReference { + function: get_promise_details.map_fn_to(), + } ]); } @@ -195,6 +198,17 @@ pub fn initialize_context<'s>( decode_val.into(), ); + let mut get_promise_details_tmpl = + v8::FunctionTemplate::new(scope, get_promise_details); + let get_promise_details_val = get_promise_details_tmpl + .get_function(scope, context) + .unwrap(); + core_val.set( + context, + v8::String::new(scope, "getPromiseDetails").unwrap().into(), + get_promise_details_val.into(), + ); + core_val.set_accessor( context, v8::String::new(scope, "shared").unwrap().into(), @@ -761,3 +775,68 @@ pub fn module_resolve_callback<'s>( None } + +// Returns promise details or throw TypeError, if argument passed isn't a Promise. +// Promise details is a two elements array. +// promise_details = [State, Result] +// State = enum { Pending = 0, Fulfilled = 1, Rejected = 2} +// Result = PromiseResult | PromiseError +fn get_promise_details( + scope: v8::FunctionCallbackScope, + args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue, +) { + let deno_isolate: &mut Isolate = + unsafe { &mut *(scope.isolate().get_data(0) as *mut Isolate) }; + assert!(!deno_isolate.global_context.is_empty()); + let context = deno_isolate.global_context.get(scope).unwrap(); + + let mut promise = match v8::Local::::try_from(args.get(0)) { + Ok(val) => val, + Err(_) => { + let msg = v8::String::new(scope, "Invalid argument").unwrap(); + let exception = v8::Exception::type_error(scope, msg); + scope.isolate().throw_exception(exception); + return; + } + }; + + let promise_details = v8::Array::new(scope, 2); + + match promise.state() { + v8::PromiseState::Pending => { + promise_details.set( + context, + v8::Integer::new(scope, 0).into(), + v8::Integer::new(scope, 0).into(), + ); + rv.set(promise_details.into()); + } + v8::PromiseState::Fulfilled => { + promise_details.set( + context, + v8::Integer::new(scope, 0).into(), + v8::Integer::new(scope, 1).into(), + ); + promise_details.set( + context, + v8::Integer::new(scope, 1).into(), + promise.result(scope), + ); + rv.set(promise_details.into()); + } + v8::PromiseState::Rejected => { + promise_details.set( + context, + v8::Integer::new(scope, 0).into(), + v8::Integer::new(scope, 2).into(), + ); + promise_details.set( + context, + v8::Integer::new(scope, 1).into(), + promise.result(scope), + ); + rv.set(promise_details.into()); + } + } +}