Skip to content

Commit

Permalink
console: print promise details (#4524)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhvsa authored Mar 30, 2020
1 parent 3892d49 commit 30fdf6d
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 1 deletion.
13 changes: 13 additions & 0 deletions cli/js/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<T>(promise: Promise<T>): promiseTypes.PromiseDetails<T>;

decode(bytes: Uint8Array): string;
encode(text: string): Uint8Array;
}
Expand Down
21 changes: 21 additions & 0 deletions cli/js/tests/console_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,27 @@ unitTest(function consoleTestStringifyIterable() {
);
});

unitTest(async function consoleTestStringifyPromises(): Promise<void> {
const pendingPromise = new Promise((_res, _rej) => {});
assertEquals(stringify(pendingPromise), "Promise { <pending> }");

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], " <rejected> Error: Whoops");
});

unitTest(function consoleTestWithCustomInspector(): void {
class A {
[customInspect](): string {
Expand Down
34 changes: 33 additions & 1 deletion cli/js/web/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
type InspectOptions = Partial<{
Expand All @@ -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";
Expand Down Expand Up @@ -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<unknown>,
ctx: ConsoleContext,
level: number,
maxLevel: number
): string {
const [state, result] = Deno.core.getPromiseDetails(value);

if (state === PromiseState.Pending) {
return "Promise { <pending> }";
}

const prefix = state === PromiseState.Fulfilled ? "" : "<rejected> ";

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(
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions cli/js/web/promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum PromiseState {
Pending = 0,
Fulfilled = 1,
Rejected = 2,
}

export type PromiseDetails<T> = [PromiseState, T | undefined];
79 changes: 79 additions & 0 deletions core/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ lazy_static! {
v8::ExternalReference {
function: decode.map_fn_to()
},
v8::ExternalReference {
function: get_promise_details.map_fn_to(),
}
]);
}

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<T> | 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::<v8::Promise>::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());
}
}
}

0 comments on commit 30fdf6d

Please sign in to comment.