Commit a913a75b authored by Maksim Sadym's avatar Maksim Sadym Committed by V8 LUCI CQ

Add `WebDriverBiDi` serialization to CDP

1. Added `generateWebDriverValue` flag to `Runtime.evaluate` and `Runtime.callFunctionOn`.
2. Added `webDriverValue` field to `RemoteObject`, and set it in case of the `generateWebDriverValue` flag was set.
3. Added virtual method `bidiSerialize` to allow embedder-implemented serialization (like in https://crrev.com/c/3472491).
4. Implemented V8 serialization in a separate class `V8WebDriverSerializer`.
5. Hardcode `max_depth=1`.
6. Added tests.

Not implemented yet:
1. `objectId`.
2. Test of embedder-implemented serialization.

Tested automatically by:
```
python3 tools/run-tests.py --outdir out/foo inspector/runtime/add-web-driver-value
```

Naming to be discussed. Suggestions are very welcome.

Design doc: http://go/bidi-serialization

Change-Id: Ib35ed8ff58e40b3304423cc2139050136d844e2c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3472077Reviewed-by: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Commit-Queue: Maksim Sadym <sadym@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79922}
parent f5ba33ad
......@@ -952,6 +952,37 @@ domain Runtime
# Unique script identifier.
type ScriptId extends string
# Represents the value serialiazed by the WebDriver BiDi specification
# https://w3c.github.io/webdriver-bidi.
type WebDriverValue extends object
properties
enum type
undefined
null
string
number
boolean
bigint
regexp
date
symbol
array
object
function
map
set
weakmap
weakset
error
proxy
promise
typedarray
arraybuffer
node
window
optional any value
optional string objectId
# Unique object identifier.
type RemoteObjectId extends string
......@@ -1004,6 +1035,8 @@ domain Runtime
optional UnserializableValue unserializableValue
# String representation of the object.
optional string description
# WebDriver BiDi representation of the value.
experimental optional WebDriverValue webDriverValue
# Unique object identifier (for non-primitive values).
optional RemoteObjectId objectId
# Preview containing abbreviated property values. Specified for `object` type values only.
......@@ -1309,6 +1342,8 @@ domain Runtime
optional string objectGroup
# Whether to throw an exception if side effect cannot be ruled out during evaluation.
experimental optional boolean throwOnSideEffect
# Whether the result should be serialized according to https://w3c.github.io/webdriver-bidi.
experimental optional boolean generateWebDriverValue
returns
# Call result.
RemoteObject result
......@@ -1394,6 +1429,8 @@ domain Runtime
# boundaries).
# This is mutually exclusive with `contextId`.
experimental optional string uniqueContextId
# Whether the result should be serialized according to https://w3c.github.io/webdriver-bidi.
experimental optional boolean generateWebDriverValue
returns
# Evaluation result.
RemoteObject result
......
......@@ -205,6 +205,15 @@ class V8_EXPORT V8InspectorSession {
virtual void triggerPreciseCoverageDeltaUpdate(StringView occasion) = 0;
};
class V8_EXPORT WebDriverValue {
public:
explicit WebDriverValue(StringView type, v8::MaybeLocal<v8::Value> value = {})
: type(type), value(value) {}
StringView type;
v8::MaybeLocal<v8::Value> value;
};
class V8_EXPORT V8InspectorClient {
public:
virtual ~V8InspectorClient() = default;
......@@ -219,6 +228,10 @@ class V8_EXPORT V8InspectorClient {
virtual void beginUserGesture() {}
virtual void endUserGesture() {}
virtual std::unique_ptr<WebDriverValue> serializeToWebDriverValue(
v8::Local<v8::Value> v8_value, int max_depth) {
return nullptr;
}
virtual std::unique_ptr<StringBuffer> valueSubtype(v8::Local<v8::Value>) {
return nullptr;
}
......
......@@ -150,6 +150,8 @@ v8_source_set("inspector") {
"v8-stack-trace-impl.h",
"v8-value-utils.cc",
"v8-value-utils.h",
"v8-webdriver-serializer.cc",
"v8-webdriver-serializer.h",
"value-mirror.cc",
"value-mirror.h",
]
......
......@@ -574,6 +574,14 @@ Response InjectedScript::wrapObjectMirror(
&customPreview);
if (customPreview) (*result)->setCustomPreview(std::move(customPreview));
}
if (wrapMode == WrapMode::kGenerateWebDriverValue) {
int maxDepth = 1;
std::unique_ptr<protocol::Runtime::WebDriverValue> webDriverValue;
response = mirror.buildWebDriverValue(context, maxDepth, &webDriverValue);
if (!response.IsSuccess()) return response;
(*result)->setWebDriverValue(std::move(webDriverValue));
}
return Response::Success();
}
......
......@@ -31,7 +31,12 @@ class V8RuntimeAgentImpl;
class V8StackTraceImpl;
struct V8StackTraceId;
enum class WrapMode { kForceValue, kNoPreview, kWithPreview };
enum class WrapMode {
kForceValue,
kNoPreview,
kWithPreview,
kGenerateWebDriverValue
};
using protocol::Response;
using TerminateExecutionCallback =
......
......@@ -256,6 +256,7 @@ void V8RuntimeAgentImpl::evaluate(
Maybe<bool> maybeAwaitPromise, Maybe<bool> throwOnSideEffect,
Maybe<double> timeout, Maybe<bool> disableBreaks, Maybe<bool> maybeReplMode,
Maybe<bool> allowUnsafeEvalBlockedByCSP, Maybe<String16> uniqueContextId,
Maybe<bool> generateWebDriverValue,
std::unique_ptr<EvaluateCallback> callback) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
"EvaluateScript");
......@@ -319,20 +320,23 @@ void V8RuntimeAgentImpl::evaluate(
return;
}
WrapMode mode = generatePreview.fromMaybe(false) ? WrapMode::kWithPreview
: WrapMode::kNoPreview;
if (returnByValue.fromMaybe(false)) mode = WrapMode::kForceValue;
WrapMode wrap_mode = generatePreview.fromMaybe(false) ? WrapMode::kWithPreview
: WrapMode::kNoPreview;
if (returnByValue.fromMaybe(false)) wrap_mode = WrapMode::kForceValue;
if (generateWebDriverValue.fromMaybe(false))
wrap_mode = WrapMode::kGenerateWebDriverValue;
// REPL mode always returns a promise that must be awaited.
const bool await = replMode || maybeAwaitPromise.fromMaybe(false);
if (!await || scope.tryCatch().HasCaught()) {
wrapEvaluateResultAsync(scope.injectedScript(), maybeResultValue,
scope.tryCatch(), objectGroup.fromMaybe(""), mode,
callback.get());
scope.tryCatch(), objectGroup.fromMaybe(""),
wrap_mode, callback.get());
return;
}
scope.injectedScript()->addPromiseCallback(
m_session, maybeResultValue, objectGroup.fromMaybe(""), mode, replMode,
m_session, maybeResultValue, objectGroup.fromMaybe(""), wrap_mode,
replMode,
EvaluateCallbackWrapper<EvaluateCallback>::wrap(std::move(callback)));
}
......@@ -366,7 +370,7 @@ void V8RuntimeAgentImpl::callFunctionOn(
Maybe<bool> silent, Maybe<bool> returnByValue, Maybe<bool> generatePreview,
Maybe<bool> userGesture, Maybe<bool> awaitPromise,
Maybe<int> executionContextId, Maybe<String16> objectGroup,
Maybe<bool> throwOnSideEffect,
Maybe<bool> throwOnSideEffect, Maybe<bool> generateWebDriverValue,
std::unique_ptr<CallFunctionOnCallback> callback) {
if (objectId.isJust() && executionContextId.isJust()) {
callback->sendFailure(Response::ServerError(
......@@ -378,9 +382,11 @@ void V8RuntimeAgentImpl::callFunctionOn(
"Either ObjectId or executionContextId must be specified"));
return;
}
WrapMode mode = generatePreview.fromMaybe(false) ? WrapMode::kWithPreview
: WrapMode::kNoPreview;
if (returnByValue.fromMaybe(false)) mode = WrapMode::kForceValue;
WrapMode wrap_mode = generatePreview.fromMaybe(false) ? WrapMode::kWithPreview
: WrapMode::kNoPreview;
if (returnByValue.fromMaybe(false)) wrap_mode = WrapMode::kForceValue;
if (generateWebDriverValue.fromMaybe(false))
wrap_mode = WrapMode::kGenerateWebDriverValue;
if (objectId.isJust()) {
InjectedScript::ObjectScope scope(m_session, objectId.fromJust());
Response response = scope.initialize();
......@@ -390,7 +396,7 @@ void V8RuntimeAgentImpl::callFunctionOn(
}
innerCallFunctionOn(
m_session, scope, scope.object(), expression,
std::move(optionalArguments), silent.fromMaybe(false), mode,
std::move(optionalArguments), silent.fromMaybe(false), wrap_mode,
userGesture.fromMaybe(false), awaitPromise.fromMaybe(false),
objectGroup.isJust() ? objectGroup.fromMaybe(String16())
: scope.objectGroupName(),
......@@ -412,7 +418,7 @@ void V8RuntimeAgentImpl::callFunctionOn(
}
innerCallFunctionOn(
m_session, scope, scope.context()->Global(), expression,
std::move(optionalArguments), silent.fromMaybe(false), mode,
std::move(optionalArguments), silent.fromMaybe(false), wrap_mode,
userGesture.fromMaybe(false), awaitPromise.fromMaybe(false),
objectGroup.fromMaybe(""), throwOnSideEffect.fromMaybe(false),
std::move(callback));
......
......@@ -75,6 +75,7 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
Maybe<double> timeout, Maybe<bool> disableBreaks,
Maybe<bool> replMode, Maybe<bool> allowUnsafeEvalBlockedByCSP,
Maybe<String16> uniqueContextId,
Maybe<bool> generateWebDriverValue,
std::unique_ptr<EvaluateCallback>) override;
void awaitPromise(const String16& promiseObjectId, Maybe<bool> returnByValue,
Maybe<bool> generatePreview,
......@@ -86,6 +87,7 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
Maybe<bool> generatePreview, Maybe<bool> userGesture,
Maybe<bool> awaitPromise, Maybe<int> executionContextId,
Maybe<String16> objectGroup, Maybe<bool> throwOnSideEffect,
Maybe<bool> generateWebDriverValue,
std::unique_ptr<CallFunctionOnCallback>) override;
Response releaseObject(const String16& objectId) override;
Response getProperties(
......
This diff is collapsed.
// Copyright 2022 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.
#ifndef V8_INSPECTOR_V8_WEBDRIVER_SERIALIZER_H_
#define V8_INSPECTOR_V8_WEBDRIVER_SERIALIZER_H_
#include "include/v8-container.h"
#include "include/v8-context.h"
#include "include/v8-exception.h"
#include "include/v8-regexp.h"
#include "src/inspector/protocol/Runtime.h"
#include "src/inspector/v8-value-utils.h"
namespace v8_inspector {
class V8WebDriverSerializer {
public:
static protocol::Response serializeV8Value(
v8::Local<v8::Object> value, v8::Local<v8::Context> context,
int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result);
};
} // namespace v8_inspector
#endif // V8_INSPECTOR_V8_WEBDRIVER_SERIALIZER_H_
......@@ -21,6 +21,7 @@
#include "src/inspector/v8-debugger.h"
#include "src/inspector/v8-inspector-impl.h"
#include "src/inspector/v8-value-utils.h"
#include "src/inspector/v8-webdriver-serializer.h"
namespace v8_inspector {
......@@ -438,6 +439,46 @@ class PrimitiveValueMirror final : public ValueMirror {
(*preview)->setSubtype(RemoteObject::SubtypeEnum::Null);
}
protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result)
const override {
// https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-primitiveProtocolValue-serialization
if (m_value->IsUndefined()) {
*result =
protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Undefined)
.build();
return Response::Success();
}
if (m_value->IsNull()) {
*result = protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Null)
.build();
return Response::Success();
}
if (m_value->IsString()) {
*result =
protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::String)
.setValue(protocol::StringValue::create(toProtocolString(
context->GetIsolate(), m_value.As<v8::String>())))
.build();
return Response::Success();
}
if (m_value->IsBoolean()) {
*result =
protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Boolean)
.setValue(protocol::FundamentalValue::create(
m_value.As<v8::Boolean>()->Value()))
.build();
return Response::Success();
}
return Response::ServerError("unexpected primitive type");
}
private:
v8::Local<v8::Value> m_value;
String16 m_type;
......@@ -488,6 +529,25 @@ class NumberMirror final : public ValueMirror {
.build();
}
protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result)
const override {
// https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-primitiveProtocolValue-serialization
*result = protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Number)
.build();
bool unserializable = false;
String16 descriptionValue = description(&unserializable);
if (unserializable) {
(*result)->setValue(protocol::StringValue::create(descriptionValue));
} else {
(*result)->setValue(toProtocolValue(m_value.As<v8::Number>()->Value()));
}
return Response::Success();
}
private:
String16 description(bool* unserializable) const {
*unserializable = true;
......@@ -548,6 +608,20 @@ class BigIntMirror final : public ValueMirror {
v8::Local<v8::Value> v8Value() const override { return m_value; }
protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result)
const override {
// https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-primitiveProtocolValue-serialization
*result = protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Bigint)
.setValue(protocol::StringValue::create(
descriptionForBigInt(context, m_value)))
.build();
return Response::Success();
}
private:
v8::Local<v8::BigInt> m_value;
};
......@@ -584,6 +658,17 @@ class SymbolMirror final : public ValueMirror {
v8::Local<v8::Value> v8Value() const override { return m_symbol; }
protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result)
const override {
// https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-RemoteValue-serialization
*result = protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Symbol)
.build();
return Response::Success();
}
private:
v8::Local<v8::Symbol> m_symbol;
};
......@@ -628,6 +713,16 @@ class LocationMirror final : public ValueMirror {
}
v8::Local<v8::Value> v8Value() const override { return m_value; }
protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result)
const override {
*result = protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Object)
.build();
return Response::Success();
}
private:
static std::unique_ptr<LocationMirror> create(v8::Local<v8::Value> value,
int scriptId, int lineNumber,
......@@ -705,6 +800,18 @@ class FunctionMirror final : public ValueMirror {
.build();
}
protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result)
const override {
// https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-RemoteValue-serialization
*result =
protocol::Runtime::WebDriverValue::create()
.setType(protocol::Runtime::WebDriverValue::TypeEnum::Function)
.build();
return Response::Success();
}
private:
v8::Local<v8::Function> m_value;
};
......@@ -990,6 +1097,41 @@ class ObjectMirror final : public ValueMirror {
if (m_hasSubtype) (*result)->setSubtype(m_subtype);
}
protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result)
const override {
// https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-RemoteValue-serialization
// Check if embedder implemented custom serialization.
std::unique_ptr<v8_inspector::WebDriverValue> embedder_serialized_result =
clientFor(context)->serializeToWebDriverValue(m_value, max_depth);
if (embedder_serialized_result) {
// Embedder-implemented serialization.
*result = protocol::Runtime::WebDriverValue::create()
.setType(toString16(embedder_serialized_result->type))
.build();
if (!embedder_serialized_result->value.IsEmpty()) {
// Embedder-implemented serialization has value.
std::unique_ptr<protocol::Value> protocol_value;
Response response = toProtocolValue(
context, embedder_serialized_result->value.ToLocalChecked(),
&protocol_value);
if (!response.IsSuccess()) return response;
(*result)->setValue(std::move(protocol_value));
}
return Response::Success();
}
// No embedder-implemented serialization. Serialize as V8 Object.
Response response = V8WebDriverSerializer::serializeV8Value(
m_value, context, max_depth, result);
return response;
}
private:
void buildObjectPreviewInternal(
v8::Local<v8::Context> context, bool forEntry,
......
......@@ -66,6 +66,9 @@ class ValueMirror {
v8::Local<v8::Context> context, int* nameLimit, int* indexLimit,
std::unique_ptr<protocol::Runtime::ObjectPreview>*) const {}
virtual v8::Local<v8::Value> v8Value() const = 0;
virtual protocol::Response buildWebDriverValue(
v8::Local<v8::Context> context, int max_depth,
std::unique_ptr<protocol::Runtime::WebDriverValue>* result) const = 0;
class PropertyAccumulator {
public:
......
This diff is collapsed.
// Copyright 2018 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('RemoteObject.webDriverValue');
Protocol.Runtime.enable();
Protocol.Runtime.onConsoleAPICalled(m => InspectorTest.logMessage(m));
InspectorTest.runAsyncTestSuite([
async function PrimitiveValue() {
await testExpression("undefined");
await testExpression("null");
await testExpression("'foo'");
await testExpression("[true, false]");
},
async function Number() {
await testExpression("[123, 0.56, -0, +Infinity, -Infinity, NaN]");
},
async function BigInt() {
await testExpression("[123n, 1234567890n]");
},
async function Symbol() {
await testExpression("Symbol('foo')");
},
async function Function() {
await testExpression("[function qwe(){}, ()=>{}]");
},
async function Array() {
await testExpression("[1,2]");
await testExpression("new Array(1,2)");
},
async function RegExp() {
await testExpression("[new RegExp('ab+c'), new RegExp('ab+c', 'ig')]");
},
async function Date() {
// Serialization depends on the timezone, so0 manual vreification is needed.
await testDate("Thu Apr 07 2022 16:16:25 GMT+1100");
await testDate("Thu Apr 07 2022 16:16:25 GMT-1100");
},
async function Error() {
await testExpression("[new Error(), new Error('qwe')]");
},
async function Map() {
await testExpression("new Map([['keyString1', {valueObject1: 1}], [{keyObject2: 2}, 'valueString2'], ['keyString3', new Array()]])");
},
async function WeakMap() {
await testExpression("new WeakMap([[{valueObject1: 1}, 'keyString1'],[{valueObject2: 2}, 'keyString2']])");
},
async function Set() {
await testExpression("new Set([{valueObject1: 1}, 'valueString2', new Array()])");
},
async function Weakset() {
await testExpression("new WeakSet([{valueObject1: 1}, {valueObject2: 2}])");
},
async function Proxy() {
await testExpression("new Proxy({}, ()=>{})");
},
async function Promise() {
await testExpression("new Promise(()=>{})");
},
async function Typedarray() {
await testExpression("new Uint16Array()");
},
async function ArrayBuffer() {
await testExpression("new ArrayBuffer()");
},
async function Object() {
// Object.
await testExpression("{nullKey: null, stringKey: 'foo',boolKey: true,numberKey: 123,bigintKey: 123n,symbolKey: Symbol('foo'),functionKey: () => {},arrayKey:[1]}");
// Object in-depth serialization.
await testExpression("{key_level_1: {key_level_2: {key_level_3: 'value_level_3'}}}");
}]);
async function testDate(dateStr) {
// TODO(sadym): make the test timezone-agnostic. Current approach is not 100% valid, as it relies on the `date.ToString` implementation.
InspectorTest.logMessage("testing date: " + dateStr);
const serializedDate = (await serializeViaEvaluate("new Date('" + dateStr + "')")).result.result.webDriverValue;
// Expected format: {
// type: "date"
// value: "Fri Apr 08 2022 03:16:25 GMT+0000 (Coordinated Universal Time)"
// }
const expectedDateStr = new Date(dateStr).toString();
InspectorTest.logMessage("Expected date in GMT: " + (new Date(dateStr).toGMTString()));
InspectorTest.logMessage("Date type as expected: " + (serializedDate.type === "date"));
if (serializedDate.value === expectedDateStr) {
InspectorTest.logMessage("Date value as expected: " + (serializedDate.value === expectedDateStr));
} else {
InspectorTest.logMessage("Error. Eexpected " + expectedDateStr + ", but was " + serializedDate.value);
}
}
async function serializeViaEvaluate(expression) {
return await Protocol.Runtime.evaluate({
expression: "("+expression+")",
generateWebDriverValue: true
});
}
async function serializeViaCallFunctionOn(expression) {
const objectId = (await Protocol.Runtime.evaluate({
expression: "({})",
generateWebDriverValue: true
})).result.result.objectId;
return await Protocol.Runtime.callFunctionOn({
functionDeclaration: "()=>{return " + expression + "}",
objectId,
generateWebDriverValue: true
});
}
async function testExpression(expression) {
InspectorTest.logMessage("testing expression: "+expression);
InspectorTest.logMessage("Runtime.evaluate");
dumpResult(await serializeViaEvaluate(expression));
InspectorTest.logMessage("Runtime.callFunctionOn");
dumpResult(await serializeViaCallFunctionOn(expression));
}
function dumpResult(result) {
if (result && result.result && result.result.result && result.result.result.webDriverValue) {
InspectorTest.logMessage(result.result.result.webDriverValue);
} else {
InspectorTest.log("...no webDriverValue...");
InspectorTest.logMessage(result);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment