Implement Runtime.callFunctionOn

Summary:
[Hermes][Inspector] Implement the CDP API for calling a function on an object.

Changelog: [Internal]

Reviewed By: avp

Differential Revision: D33722301

fbshipit-source-id: da26e865cf29920be77c5c602dde1b443b4c64da
This commit is contained in:
John Porto
2022-02-01 16:48:54 -08:00
committed by Facebook GitHub Bot
parent cdfddb4dad
commit eb08af57a9
5 changed files with 693 additions and 10 deletions
@@ -14,6 +14,7 @@
#include <folly/Conv.h>
#include <folly/Executor.h>
#include <folly/Function.h>
#include <folly/json.h>
#include <glog/logging.h>
#include <hermes/inspector/Inspector.h>
#include <hermes/inspector/chrome/MessageConverters.h>
@@ -100,11 +101,15 @@ class Connection::Impl : public inspector::InspectorObserver,
void handle(
const m::heapProfiler::GetObjectByHeapObjectIdRequest &req) override;
void handle(const m::heapProfiler::GetHeapObjectIdRequest &req) override;
void handle(const m::runtime::CallFunctionOnRequest &req) override;
void handle(const m::runtime::EvaluateRequest &req) override;
void handle(const m::runtime::GetPropertiesRequest &req) override;
void handle(const m::runtime::RunIfWaitingForDebuggerRequest &req) override;
private:
// The execution context id reported back by the ExecutionContextCreated
// notification. We only ever expect this execution context id.
static constexpr int32_t kHermesExecutionContextId = 1;
std::vector<m::runtime::PropertyDescriptor> makePropsFromScope(
std::pair<uint32_t, uint32_t> frameAndScopeIndex,
const std::string &objectGroup,
@@ -291,7 +296,7 @@ void Connection::Impl::onContextCreated(Inspector &inspector) {
// Right now, Hermes only has the notion of one JS context per VM instance,
// so we just always name the single JS context with id=1 and name=hermes.
m::runtime::ExecutionContextCreatedNotification note;
note.context.id = 1;
note.context.id = kHermesExecutionContextId;
note.context.name = "hermes";
sendNotificationToClientViaExecutor(note);
@@ -370,6 +375,8 @@ void Connection::Impl::onScriptParsed(
m::debugger::ScriptParsedNotification note;
note.scriptId = folly::to<std::string>(info.fileId);
note.url = info.fileName;
// TODO(jpporto): fix test cases sending invalid context id.
// note.executionContextId = kHermesExecutionContextId;
if (!info.sourceMappingUrl.empty()) {
note.sourceMapURL = info.sourceMappingUrl;
@@ -397,6 +404,8 @@ void Connection::Impl::onMessageAdded(
const ConsoleMessageInfo &info) {
m::runtime::ConsoleAPICalledNotification apiCalledNote;
apiCalledNote.type = info.level;
// TODO(jpporto): fix test cases sending invalid context id.
// apiCalledNote.executionContextId = kHermesExecutionContextId;
size_t argsSize = info.args.size(getRuntime());
for (size_t index = 0; index < argsSize; ++index) {
@@ -711,6 +720,315 @@ void Connection::Impl::handle(
.thenError<std::exception>(sendErrorToClient(req.id));
}
namespace {
/// Runtime.CallArguments can have their values specified "inline", or they can
/// have remote references. The inline values are eval'd together with the
/// Runtime.CallFunctionOn.functionDeclaration (see CallFunctionOnBuilder
/// below), while remote object Ids need to be resolved outside of the VM.
class CallFunctionOnArgument {
public:
explicit CallFunctionOnArgument(
folly::Optional<m::runtime::RemoteObjectId> maybeObjectId)
: maybeObjectId_(std::move(maybeObjectId)) {}
/// Computes the real value for this argument, which can be an object
/// referenced by maybeObjectId_, or the given evaldValue. Throws if
/// maybeObjectId_ is not empty but references an unknown object.
jsi::Value value(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
jsi::Value evaldValue) const {
if (maybeObjectId_) {
assert(evaldValue.isUndefined() && "expected undefined placeholder");
return getValueFromId(rt, objTable, *maybeObjectId_);
}
return evaldValue;
}
private:
/// Returns the jsi::Object for the given objId. Throws if such object can't
/// be found.
static jsi::Value getValueFromId(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
m::runtime::RemoteObjectId objId) {
if (const jsi::Value *ptr = objTable.getValue(objId)) {
return jsi::Value(rt, *ptr);
}
throw std::runtime_error("unknown object id " + objId);
}
folly::Optional<m::runtime::RemoteObjectId> maybeObjectId_;
};
/// Functor that should be used to run the result of eval-ing a CallFunctionOn
/// request.
class CallFunctionOnRunner {
public:
static constexpr size_t kJsThisIndex = 0;
static constexpr size_t kFirstArgIndex = 1;
// N.B.: constexpr char[] broke react-native-oss-android.
static const char *kJsThisArgPlaceholder;
CallFunctionOnRunner() = default;
CallFunctionOnRunner(CallFunctionOnRunner &&) = default;
CallFunctionOnRunner &operator=(CallFunctionOnRunner &&) = default;
/// Performs the actual Runtime.CallFunctionOn request. It assumes.
/// \p evalResult is the result of invoking the Inspector's evaluate() method
/// on the expression built by the CallFunctionOnBuilder below.
jsi::Value operator()(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
const facebook::hermes::debugger::EvalResult &evalResult) {
// The eval result is an array [a0, a1, ..., an, func] (see
// CallFunctionOnBuilder below).
auto argsAndFunc = evalResult.value.getObject(rt).getArray(rt);
assert(
argsAndFunc.length(rt) == thisAndArguments_.size() + 1 &&
"Unexpected result size");
// now resolve the arguments to the call, including "this".
std::vector<jsi::Value> arguments(thisAndArguments_.size() - 1);
jsi::Object jsThis =
getJsThis(rt, objTable, argsAndFunc.getValueAtIndex(rt, kJsThisIndex));
int i = kFirstArgIndex;
for (/*i points to the first param*/; i < thisAndArguments_.size(); ++i) {
arguments[i - kFirstArgIndex] = thisAndArguments_[i].value(
rt, objTable, argsAndFunc.getValueAtIndex(rt, i));
}
// i is now func's index.
jsi::Function func =
argsAndFunc.getValueAtIndex(rt, i).getObject(rt).getFunction(rt);
return func.callWithThis(
rt,
std::move(jsThis),
static_cast<const jsi::Value *>(arguments.data()),
arguments.size());
}
private:
friend class CallFunctionOnBuilder;
CallFunctionOnRunner(const CallFunctionOnRunner &) = delete;
CallFunctionOnRunner &operator=(const CallFunctionOnRunner &) = delete;
CallFunctionOnRunner(
std::vector<CallFunctionOnArgument> thisAndArguments,
folly::Optional<m::runtime::ExecutionContextId> executionContextId)
: thisAndArguments_(std::move(thisAndArguments)),
executionContextId_(std::move(executionContextId)) {}
/// Resolves the js "this" for the request, which lives in
/// thisAndArguments_[kJsThisIndex]. \p evaldThis should either be undefined,
/// or the placeholder indicating that globalThis should be used.
jsi::Object getJsThis(
jsi::Runtime &rt,
RemoteObjectsTable &objTable,
jsi::Value evaldThis) const {
// In the future we may support multiple execution context ids; for now,
// there's only one.
(void)executionContextId_;
// Either evaldThis is undefined (because the request had an object id
// specifying "this"), or it should be a string (i.e., the placeholder
// kJsThisArgPlaceholder).
assert(
(evaldThis.isUndefined() ||
(evaldThis.isString() &&
evaldThis.getString(rt).utf8(rt) == kJsThisArgPlaceholder)) &&
"unexpected value for jsThis argument placeholder");
// Need to save this information because of the std::move() below.
const bool useGlobalThis = evaldThis.isString();
jsi::Value value = thisAndArguments_[kJsThisIndex].value(
rt, objTable, std::move(evaldThis));
return useGlobalThis ? rt.global() : value.getObject(rt);
}
std::vector<CallFunctionOnArgument> thisAndArguments_;
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
};
/*static*/ const char *CallFunctionOnRunner::kJsThisArgPlaceholder =
"jsThis is Execution Context";
/// Returns true if \p str is a number-like string value (e.g., Infinity),
/// and false otherwise.
bool unserializableValueLooksLikeNumber(const std::string &str) {
return str == "Infinity" || str == "-Infinity" || str == "NaN";
}
/// Helper class that processes a Runtime.CallFunctionOn request, and
/// builds an expression string that, once eval()d, yields an Array with the
/// CallArguments as well as the function to run. The generated array is
///
/// [JsThis, P0, P1, P2, P3, Pn, F]
///
/// where:
/// * F is the functionDeclaration in the request
/// * JsThis is either:
/// * undefined (if the request has an object ID); or
/// * the placeholder kJsThisArgPlaceholder
/// * Pi is either:
/// * the string in CallArgument[i].unserializableValue; or
/// * the string in CallArgument[i].value; or
/// * arguments[j] (i.e., the j-th argument passed to the newly built
/// function), j being the j-th CallArgument with an ObjectId. This is
/// needed because there's no easy way to express the objects referred
/// to by object ids by name.
class CallFunctionOnBuilder {
public:
explicit CallFunctionOnBuilder(const m::runtime::CallFunctionOnRequest &req)
: executionContextId_(req.executionContextId) {
out_ << "[";
thisAndArguments_.emplace_back(CallFunctionOnArgument(req.objectId));
if (req.objectId) {
out_ << "undefined, ";
} else {
out_ << '\'' << CallFunctionOnRunner::kJsThisArgPlaceholder << "', ";
}
addParams(req.arguments);
out_ << req.functionDeclaration;
out_ << "]";
};
/// Extracts the functions that handles the CallFunctionOn requests, as well
/// as the list of object ids that must be passed when calling it.
std::pair<std::string, CallFunctionOnRunner> expressionAndRunner() && {
return std::make_pair(
std::move(out_).str(),
CallFunctionOnRunner(
std::move(thisAndArguments_), std::move(executionContextId_)));
}
private:
void addParams(const folly::Optional<std::vector<m::runtime::CallArgument>>
&maybeArguments) {
if (maybeArguments) {
for (const auto &ca : *maybeArguments) {
addParam(ca);
thisAndArguments_.emplace_back(CallFunctionOnArgument(ca.objectId));
out_ << ", ";
}
}
}
void addParam(const m::runtime::CallArgument &ca) {
if (ca.objectId) {
out_ << "undefined";
} else if (ca.value) {
// TODO: this may throw if ca.value is a CBOR (see RFC 8949), but the
// chrome debugger doesn't seem to send those.
out_ << "(" << folly::toJson(*ca.value) << ")";
} else if (ca.unserializableValue) {
if (unserializableValueLooksLikeNumber(*ca.unserializableValue)) {
out_ << "+(" << *ca.unserializableValue << ")";
} else {
out_ << *ca.unserializableValue;
}
} else {
throw std::runtime_error("unknown payload for CallParam");
}
}
std::ostringstream out_;
std::vector<CallFunctionOnArgument> thisAndArguments_;
folly::Optional<m::runtime::ExecutionContextId> executionContextId_;
};
} // namespace
void Connection::Impl::handle(const m::runtime::CallFunctionOnRequest &req) {
std::string expression;
CallFunctionOnRunner runner;
auto validateAndParseRequest =
[&expression, &runner](const m::runtime::CallFunctionOnRequest &req)
-> folly::Optional<std::string> {
if (req.objectId.hasValue() == req.executionContextId.hasValue()) {
return std::string(
"The request must specify either object id or execution context id.");
}
if (!req.objectId) {
assert(
req.executionContextId &&
"should not be here if both object id and execution context id are missing");
if (*req.executionContextId != kHermesExecutionContextId) {
return "unknown execution context id " +
std::to_string(*req.executionContextId);
}
}
try {
std::tie(expression, runner) =
CallFunctionOnBuilder(req).expressionAndRunner();
} catch (const std::exception &e) {
return std::string(e.what());
}
return {};
};
if (auto errMsg = validateAndParseRequest(req)) {
sendErrorToClientViaExecutor(req.id, *errMsg);
return;
}
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
inspector_
->evaluate(
0, // Top of the stackframe
expression,
[this,
remoteObjPtr,
objectGroup = req.objectGroup,
jsThisId = req.objectId,
executionContextId = req.executionContextId,
byValue = req.returnByValue.value_or(false),
runner =
std::move(runner)](const facebook::hermes::debugger::EvalResult
&evalResult) mutable {
if (evalResult.isException) {
return;
}
*remoteObjPtr = m::runtime::makeRemoteObject(
getRuntime(),
runner(getRuntime(), objTable_, evalResult),
objTable_,
objectGroup.value_or("ConsoleObjectGroup"),
byValue);
})
.via(executor_.get())
.thenValue(
[this, id = req.id, remoteObjPtr](debugger::EvalResult result) {
m::debugger::EvaluateOnCallFrameResponse resp;
resp.id = id;
if (result.isException) {
resp.exceptionDetails =
m::runtime::makeExceptionDetails(result.exceptionDetails);
} else {
resp.result = *remoteObjPtr;
}
sendResponseToClient(resp);
})
.thenError<std::exception>(sendErrorToClient(req.id));
}
void Connection::Impl::handle(const m::runtime::EvaluateRequest &req) {
auto remoteObjPtr = std::make_shared<m::runtime::RemoteObject>();
@@ -1,5 +1,5 @@
// Copyright 2004-present Facebook. All Rights Reserved.
// @generated SignedSource<<522f29c54f207a4f7b5c33af07cf64d0>>
// @generated <<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>
#include "MessageTypes.h"
@@ -60,6 +60,7 @@ std::unique_ptr<Request> Request::fromJsonThrowOnError(const std::string &str) {
makeUnique<heapProfiler::StopTrackingHeapObjectsRequest>},
{"HeapProfiler.takeHeapSnapshot",
makeUnique<heapProfiler::TakeHeapSnapshotRequest>},
{"Runtime.callFunctionOn", makeUnique<runtime::CallFunctionOnRequest>},
{"Runtime.evaluate", makeUnique<runtime::EvaluateRequest>},
{"Runtime.getProperties", makeUnique<runtime::GetPropertiesRequest>},
{"Runtime.runIfWaitingForDebugger",
@@ -274,6 +275,21 @@ dynamic heapProfiler::SamplingHeapProfile::toDynamic() const {
return obj;
}
runtime::CallArgument::CallArgument(const dynamic &obj) {
assign(value, obj, "value");
assign(unserializableValue, obj, "unserializableValue");
assign(objectId, obj, "objectId");
}
dynamic runtime::CallArgument::toDynamic() const {
dynamic obj = dynamic::object;
put(obj, "value", value);
put(obj, "unserializableValue", unserializableValue);
put(obj, "objectId", objectId);
return obj;
}
runtime::ExecutionContextDescription::ExecutionContextDescription(
const dynamic &obj) {
assign(id, obj, "id");
@@ -931,6 +947,49 @@ void heapProfiler::TakeHeapSnapshotRequest::accept(
handler.handle(*this);
}
runtime::CallFunctionOnRequest::CallFunctionOnRequest()
: Request("Runtime.callFunctionOn") {}
runtime::CallFunctionOnRequest::CallFunctionOnRequest(const dynamic &obj)
: Request("Runtime.callFunctionOn") {
assign(id, obj, "id");
assign(method, obj, "method");
dynamic params = obj.at("params");
assign(functionDeclaration, params, "functionDeclaration");
assign(objectId, params, "objectId");
assign(arguments, params, "arguments");
assign(silent, params, "silent");
assign(returnByValue, params, "returnByValue");
assign(userGesture, params, "userGesture");
assign(awaitPromise, params, "awaitPromise");
assign(executionContextId, params, "executionContextId");
assign(objectGroup, params, "objectGroup");
}
dynamic runtime::CallFunctionOnRequest::toDynamic() const {
dynamic params = dynamic::object;
put(params, "functionDeclaration", functionDeclaration);
put(params, "objectId", objectId);
put(params, "arguments", arguments);
put(params, "silent", silent);
put(params, "returnByValue", returnByValue);
put(params, "userGesture", userGesture);
put(params, "awaitPromise", awaitPromise);
put(params, "executionContextId", executionContextId);
put(params, "objectGroup", objectGroup);
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "method", method);
put(obj, "params", std::move(params));
return obj;
}
void runtime::CallFunctionOnRequest::accept(RequestHandler &handler) const {
handler.handle(*this);
}
runtime::EvaluateRequest::EvaluateRequest() : Request("Runtime.evaluate") {}
runtime::EvaluateRequest::EvaluateRequest(const dynamic &obj)
@@ -1187,6 +1246,25 @@ dynamic heapProfiler::StopSamplingResponse::toDynamic() const {
return obj;
}
runtime::CallFunctionOnResponse::CallFunctionOnResponse(const dynamic &obj) {
assign(id, obj, "id");
dynamic res = obj.at("result");
assign(result, res, "result");
assign(exceptionDetails, res, "exceptionDetails");
}
dynamic runtime::CallFunctionOnResponse::toDynamic() const {
dynamic res = dynamic::object;
put(res, "result", result);
put(res, "exceptionDetails", exceptionDetails);
dynamic obj = dynamic::object;
put(obj, "id", id);
put(obj, "result", std::move(res));
return obj;
}
runtime::EvaluateResponse::EvaluateResponse(const dynamic &obj) {
assign(id, obj, "id");
@@ -1,5 +1,5 @@
// Copyright 2004-present Facebook. All Rights Reserved.
// @generated SignedSource<<a541d174394c8959b9fb6a7c575e7040>>
// @generated <<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>
#pragma once
@@ -48,7 +48,10 @@ struct StepOverRequest;
} // namespace debugger
namespace runtime {
struct CallArgument;
struct CallFrame;
struct CallFunctionOnRequest;
struct CallFunctionOnResponse;
struct ConsoleAPICalledNotification;
struct EvaluateRequest;
struct EvaluateResponse;
@@ -122,6 +125,7 @@ struct RequestHandler {
virtual void handle(
const heapProfiler::StopTrackingHeapObjectsRequest &req) = 0;
virtual void handle(const heapProfiler::TakeHeapSnapshotRequest &req) = 0;
virtual void handle(const runtime::CallFunctionOnRequest &req) = 0;
virtual void handle(const runtime::EvaluateRequest &req) = 0;
virtual void handle(const runtime::GetPropertiesRequest &req) = 0;
virtual void handle(const runtime::RunIfWaitingForDebuggerRequest &req) = 0;
@@ -156,6 +160,7 @@ struct NoopRequestHandler : public RequestHandler {
void handle(
const heapProfiler::StopTrackingHeapObjectsRequest &req) override {}
void handle(const heapProfiler::TakeHeapSnapshotRequest &req) override {}
void handle(const runtime::CallFunctionOnRequest &req) override {}
void handle(const runtime::EvaluateRequest &req) override {}
void handle(const runtime::GetPropertiesRequest &req) override {}
void handle(const runtime::RunIfWaitingForDebuggerRequest &req) override {}
@@ -281,6 +286,16 @@ struct heapProfiler::SamplingHeapProfile : public Serializable {
std::vector<heapProfiler::SamplingHeapProfileSample> samples;
};
struct runtime::CallArgument : public Serializable {
CallArgument() = default;
explicit CallArgument(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
folly::Optional<folly::dynamic> value;
folly::Optional<runtime::UnserializableValue> unserializableValue;
folly::Optional<runtime::RemoteObjectId> objectId;
};
struct runtime::ExecutionContextDescription : public Serializable {
ExecutionContextDescription() = default;
explicit ExecutionContextDescription(const folly::dynamic &obj);
@@ -546,6 +561,24 @@ struct heapProfiler::TakeHeapSnapshotRequest : public Request {
folly::Optional<bool> treatGlobalObjectsAsRoots;
};
struct runtime::CallFunctionOnRequest : public Request {
CallFunctionOnRequest();
explicit CallFunctionOnRequest(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
void accept(RequestHandler &handler) const override;
std::string functionDeclaration;
folly::Optional<runtime::RemoteObjectId> objectId;
folly::Optional<std::vector<runtime::CallArgument>> arguments;
folly::Optional<bool> silent;
folly::Optional<bool> returnByValue;
folly::Optional<bool> userGesture;
folly::Optional<bool> awaitPromise;
folly::Optional<runtime::ExecutionContextId> executionContextId;
folly::Optional<std::string> objectGroup;
};
struct runtime::EvaluateRequest : public Request {
EvaluateRequest();
explicit EvaluateRequest(const folly::dynamic &obj);
@@ -658,6 +691,15 @@ struct heapProfiler::StopSamplingResponse : public Response {
heapProfiler::SamplingHeapProfile profile{};
};
struct runtime::CallFunctionOnResponse : public Response {
CallFunctionOnResponse() = default;
explicit CallFunctionOnResponse(const folly::dynamic &obj);
folly::dynamic toDynamic() const override;
runtime::RemoteObject result{};
folly::Optional<runtime::ExceptionDetails> exceptionDetails;
};
struct runtime::EvaluateResponse : public Response {
EvaluateResponse() = default;
explicit EvaluateResponse(const folly::dynamic &obj);
@@ -407,7 +407,7 @@ std::unordered_map<std::string, std::string> expectProps(
m::runtime::PropertyDescriptor &desc = resp.result[i];
auto infoIt = infos.find(desc.name);
EXPECT_FALSE(infoIt == infos.end());
EXPECT_FALSE(infoIt == infos.end()) << desc.name;
if (infoIt != infos.end()) {
const PropInfo &info = infoIt->second;
@@ -460,6 +460,25 @@ void expectEvalResponse(
expectProps(conn, id + 1, resp.result.objectId.value(), infos);
}
m::runtime::CallArgument makeValueCallArgument(folly::dynamic val) {
m::runtime::CallArgument ret;
ret.value = val;
return ret;
}
m::runtime::CallArgument makeUnserializableCallArgument(std::string val) {
m::runtime::CallArgument ret;
ret.unserializableValue = std::move(val);
return ret;
}
m::runtime::CallArgument makeObjectIdCallArgument(
m::runtime::RemoteObjectId objectId) {
m::runtime::CallArgument ret;
ret.objectId = std::move(objectId);
return ret;
}
} // namespace
TEST(ConnectionTests, testRespondsOkToUnknownRequests) {
@@ -1981,6 +2000,231 @@ TEST(ConnectionTests, testScopeVariables) {
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testRuntimeCallFunctionOnObject) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// create a new Object() that will be used as "this" below.
m::runtime::RemoteObjectId thisId;
{
sendRuntimeEvalRequest(conn, msgId, "new Object()");
auto resp = expectResponse<m::runtime::EvaluateResponse>(conn, msgId++);
ASSERT_TRUE(resp.result.objectId) << resp.toDynamic();
thisId = *resp.result.objectId;
}
// expectedPropInfos are properties that are expected to exist in thisId. It
// is modified by addMember (below).
std::unordered_map<std::string, PropInfo> expectedPropInfos;
// Add __proto__ as it always exists.
expectedPropInfos.emplace("__proto__", PropInfo("object"));
/// addMember sends Runtime.callFunctionOn() requests with a function
/// declaration that simply adds a new property called \p propName with type
/// \p type to the remote object \p id. \p ca is the property's value.
/// The new property must not exist in \p id unless \p allowRedefinition is
/// true.
auto addMember = [&](const m::runtime::RemoteObjectId id,
const char *type,
const char *propName,
const m::runtime::CallArgument &ca,
bool allowRedefinition = false) {
m::runtime::CallFunctionOnRequest req;
req.id = msgId++;
req.functionDeclaration =
std::string("function(e){const r=\"") + propName + "\"; this[r]=e,r}";
req.arguments = std::vector<m::runtime::CallArgument>{ca};
req.objectId = thisId;
conn.send(req.toJson());
expectResponse<m::runtime::CallFunctionOnResponse>(conn, req.id);
auto it = expectedPropInfos.emplace(propName, PropInfo(type));
EXPECT_TRUE(allowRedefinition || it.second)
<< "property \"" << propName << "\" redefined.";
if (ca.value) {
it.first->second.setValue(*ca.value);
}
if (ca.unserializableValue) {
it.first->second.setUnserializableValue(*ca.unserializableValue);
}
};
addMember(thisId, "boolean", "b", makeValueCallArgument(true));
addMember(thisId, "number", "num", makeValueCallArgument(12));
addMember(thisId, "string", "str", makeValueCallArgument("string value"));
addMember(thisId, "object", "self_ref", makeObjectIdCallArgument(thisId));
addMember(
thisId, "number", "inf", makeUnserializableCallArgument("Infinity"));
addMember(
thisId, "number", "ni", makeUnserializableCallArgument("-Infinity"));
addMember(thisId, "number", "nan", makeUnserializableCallArgument("NaN"));
/// ensures that \p objId has all of the expected properties; Returns the
/// runtime::RemoteObjectId for the "self_ref" property (which must exist).
auto verifyObjShape = [&](const m::runtime::RemoteObjectId &objId)
-> folly::Optional<std::string> {
auto objProps = expectProps(conn, msgId++, objId, expectedPropInfos);
EXPECT_TRUE(objProps.count("__proto__"));
auto objPropIt = objProps.find("self_ref");
if (objPropIt == objProps.end()) {
EXPECT_TRUE(false) << "missing \"self_ref\" property.";
return {};
}
return objPropIt->second;
};
// Verify that thisId has the correct shape.
auto selfRefId = verifyObjShape(thisId);
ASSERT_TRUE(selfRefId);
// Then verify that the self reference has the correct shape. If thisId does
// not have the "self_ref" property the call to verifyObjShape will return an
// empty Optional, as well as report an error.
selfRefId = verifyObjShape(*selfRefId);
ASSERT_TRUE(selfRefId);
// Now we modify the self reference, which should cause thisId to change
// as well.
const bool kAllowRedefinition = true;
addMember(
*selfRefId,
"number",
"num",
makeValueCallArgument(42),
kAllowRedefinition);
addMember(
*selfRefId, "number", "neg_zero", makeUnserializableCallArgument("-0"));
verifyObjShape(thisId);
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testRuntimeCallFunctionOnExecutionContext) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger;
)");
/// helper that returns a map with all of \p objId 's members.
auto getProps = [&msgId, &conn](const m::runtime::RemoteObjectId &objId) {
m::runtime::GetPropertiesRequest req;
req.id = msgId++;
req.objectId = objId;
conn.send(req.toJson());
auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, req.id);
std::unordered_map<std::string, folly::Optional<m::runtime::RemoteObject>>
properties;
for (auto propertyDescriptor : resp.result) {
properties[propertyDescriptor.name] = propertyDescriptor.value;
}
return properties;
};
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// globalThisId is the inspector's object Id for globalThis.
m::runtime::RemoteObjectId globalThisId;
{
sendRuntimeEvalRequest(conn, msgId, "globalThis");
auto resp = expectResponse<m::runtime::EvaluateResponse>(conn, msgId++);
ASSERT_TRUE(resp.result.objectId) << resp.toDynamic();
globalThisId = *resp.result.objectId;
}
// This test table has all of the new fields we want to add to globalThis,
// plus the Runtime.CallArgument to be sent to the inspector.
struct {
const char *propName;
const m::runtime::CallArgument callArg;
} tests[] = {
{"callFunctionOnTestMember1", makeValueCallArgument(10)},
{"callFunctionOnTestMember2", makeValueCallArgument("string")},
{"callFunctionOnTestMember3", makeUnserializableCallArgument("NaN")},
{"callFunctionOnTestMember4", makeUnserializableCallArgument("-0")},
};
// sanity-check that our test fields don't exist in global this.
{
auto currProps = getProps(globalThisId);
for (const auto &test : tests) {
EXPECT_EQ(currProps.count(test.propName), 0) << test.propName;
}
}
auto addMember = [&msgId, &conn](
const char *propName,
const m::runtime::CallArgument &ca) {
m::runtime::CallFunctionOnRequest req;
req.id = msgId++;
req.functionDeclaration =
std::string("function(e){const r=\"") + propName + "\"; this[r]=e,r}";
req.arguments = std::vector<m::runtime::CallArgument>{ca};
req.executionContextId = 1;
conn.send(req.toJson());
expectResponse<m::runtime::CallFunctionOnResponse>(conn, req.id);
};
for (const auto &test : tests) {
addMember(test.propName, test.callArg);
}
{
auto currProps = getProps(globalThisId);
for (const auto &test : tests) {
auto it = currProps.find(test.propName);
// there should be a property named test.propName in globalThis.
ASSERT_TRUE(it != currProps.end()) << test.propName;
// and it should have a value.
ASSERT_TRUE(it->second) << test.propName;
if (it->second->value.hasValue()) {
// the property has a value, so make sure that's what's being expected.
auto actual = it->second->value;
auto expected = test.callArg.value;
ASSERT_TRUE(expected.hasValue()) << test.propName;
EXPECT_EQ(*actual, *expected) << test.propName;
} else if (it->second->unserializableValue.hasValue()) {
// the property has an unserializable value, so make sure that's what's
// being expected.
auto actual = it->second->unserializableValue;
auto expected = test.callArg.unserializableValue;
ASSERT_TRUE(expected.hasValue()) << test.propName;
EXPECT_EQ(*actual, *expected) << test.propName;
} else {
FAIL() << "No value or unserializable value in " << test.propName;
}
}
}
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testConsoleLog) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
@@ -2489,9 +2733,9 @@ TEST(ConnectionTests, heapProfilerSampling) {
req.id = msgId++;
// Sample every 256 bytes to ensure there are some samples. The default is
// 32768, which is too high for a small example. Note that sampling is a
// random process, so there's no guarantee there will be any samples in any
// finite number of allocations. In practice the likelihood is so high that
// there shouldn't be any issues.
// random process, so there's no guarantee there will be any samples in
// any finite number of allocations. In practice the likelihood is so high
// that there shouldn't be any issues.
req.samplingInterval = 256;
send(conn, req);
}
@@ -2582,9 +2826,9 @@ TEST(ConnectionTests, heapSnapshotRemoteObject) {
testObject(storedObjID, "object", "Array", "Array(3)", "array");
// Force a collection to move the heap.
runtime->instrumentation().collectGarbage("test");
// A collection should not disturb the unique ID lookup, and it should be the
// same object as before. Note that it won't have the same remote ID, because
// Hermes doesn't do uniquing.
// A collection should not disturb the unique ID lookup, and it should be
// the same object as before. Note that it won't have the same remote ID,
// because Hermes doesn't do uniquing.
testObject(globalObjID, "object", "Object", "Object", nullptr);
testObject(storedObjID, "object", "Array", "Array(3)", "array");
@@ -28,6 +28,7 @@ HeapProfiler.heapStatsUpdate
HeapProfiler.lastSeenObjectId
HeapProfiler.getObjectByHeapObjectId
HeapProfiler.getHeapObjectId
Runtime.callFunctionOn
Runtime.consoleAPICalled
Runtime.evaluate
Runtime.executionContextCreated