Blink-compatible serialization of arrays, both dense and sparse.

The current "dense" format is not expressive enough to distinguish between
an element that is not defined and one that has the value "undefined",
but in this CL the existing behaviour of Blink is used for such cases.
Format changes to fix these issues could be made later on.

Not included in this CL is compatibility with version 0 arrays.
Those will be implemented in a separate CL.

BUG=chromium:148757

Review-Url: https://codereview.chromium.org/2259633002
Cr-Commit-Position: refs/heads/master@{#38729}
This commit is contained in:
jbroman 2016-08-18 11:47:04 -07:00 committed by Commit bot
parent 06520ec220
commit 2e000127df
3 changed files with 477 additions and 0 deletions

View File

@ -60,6 +60,16 @@ enum class SerializationTag : uint8_t {
kBeginJSObject = 'o',
// End of a JS object. numProperties:uint32_t
kEndJSObject = '{',
// Beginning of a sparse JS array. length:uint32_t
// Elements and properties are written as key/value pairs, like objects.
kBeginSparseJSArray = 'a',
// End of a sparse JS array. numProperties:uint32_t length:uint32_t
kEndSparseJSArray = '@',
// Beginning of a dense JS array. length:uint32_t
// |length| elements, followed by properties as key/value pairs
kBeginDenseJSArray = 'A',
// End of a dense JS array. numProperties:uint32_t length:uint32_t
kEndDenseJSArray = '$',
};
ValueSerializer::ValueSerializer(Isolate* isolate)
@ -253,6 +263,8 @@ Maybe<bool> ValueSerializer::WriteJSReceiver(Handle<JSReceiver> receiver) {
HandleScope scope(isolate_);
switch (instance_type) {
case JS_ARRAY_TYPE:
return WriteJSArray(Handle<JSArray>::cast(receiver));
case JS_OBJECT_TYPE:
case JS_API_OBJECT_TYPE:
return WriteJSObject(Handle<JSObject>::cast(receiver));
@ -278,6 +290,67 @@ Maybe<bool> ValueSerializer::WriteJSObject(Handle<JSObject> object) {
return Just(true);
}
Maybe<bool> ValueSerializer::WriteJSArray(Handle<JSArray> array) {
uint32_t length;
array->length()->ToArrayLength(&length);
// To keep things simple, for now we decide between dense and sparse
// serialization based on elements kind. A more principled heuristic could
// count the elements, but would need to take care to note which indices
// existed (as only indices which were enumerable own properties at this point
// should be serialized).
const bool should_serialize_densely =
array->HasFastElements() && !array->HasFastHoleyElements();
if (should_serialize_densely) {
// TODO(jbroman): Distinguish between undefined and a hole (this can happen
// if serializing one of the elements deletes another). This requires wire
// format changes.
WriteTag(SerializationTag::kBeginDenseJSArray);
WriteVarint<uint32_t>(length);
for (uint32_t i = 0; i < length; i++) {
// Serializing the array's elements can have arbitrary side effects, so we
// cannot rely on still having fast elements, even if it did to begin
// with.
Handle<Object> element;
LookupIterator it(isolate_, array, i, array, LookupIterator::OWN);
if (!Object::GetProperty(&it).ToHandle(&element) ||
!WriteObject(element).FromMaybe(false)) {
return Nothing<bool>();
}
}
KeyAccumulator accumulator(isolate_, KeyCollectionMode::kOwnOnly,
ENUMERABLE_STRINGS);
if (!accumulator.CollectOwnPropertyNames(array, array).FromMaybe(false)) {
return Nothing<bool>();
}
Handle<FixedArray> keys =
accumulator.GetKeys(GetKeysConversion::kConvertToString);
uint32_t properties_written;
if (!WriteJSObjectProperties(array, keys).To(&properties_written)) {
return Nothing<bool>();
}
WriteTag(SerializationTag::kEndDenseJSArray);
WriteVarint<uint32_t>(properties_written);
WriteVarint<uint32_t>(length);
} else {
WriteTag(SerializationTag::kBeginSparseJSArray);
WriteVarint<uint32_t>(length);
Handle<FixedArray> keys;
uint32_t properties_written;
if (!KeyAccumulator::GetKeys(array, KeyCollectionMode::kOwnOnly,
ENUMERABLE_STRINGS)
.ToHandle(&keys) ||
!WriteJSObjectProperties(array, keys).To(&properties_written)) {
return Nothing<bool>();
}
WriteTag(SerializationTag::kEndSparseJSArray);
WriteVarint<uint32_t>(properties_written);
WriteVarint<uint32_t>(length);
}
return Just(true);
}
Maybe<uint32_t> ValueSerializer::WriteJSObjectProperties(
Handle<JSObject> object, Handle<FixedArray> keys) {
uint32_t properties_written = 0;
@ -454,6 +527,10 @@ MaybeHandle<Object> ValueDeserializer::ReadObject() {
}
case SerializationTag::kBeginJSObject:
return ReadJSObject();
case SerializationTag::kBeginSparseJSArray:
return ReadSparseJSArray();
case SerializationTag::kBeginDenseJSArray:
return ReadDenseJSArray();
default:
return MaybeHandle<Object>();
}
@ -517,6 +594,71 @@ MaybeHandle<JSObject> ValueDeserializer::ReadJSObject() {
return scope.CloseAndEscape(object);
}
MaybeHandle<JSArray> ValueDeserializer::ReadSparseJSArray() {
// If we are at the end of the stack, abort. This function may recurse.
if (StackLimitCheck(isolate_).HasOverflowed()) return MaybeHandle<JSArray>();
uint32_t length;
if (!ReadVarint<uint32_t>().To(&length)) return MaybeHandle<JSArray>();
uint32_t id = next_id_++;
HandleScope scope(isolate_);
Handle<JSArray> array = isolate_->factory()->NewJSArray(0);
JSArray::SetLength(array, length);
AddObjectWithID(id, array);
uint32_t num_properties;
uint32_t expected_num_properties;
uint32_t expected_length;
if (!ReadJSObjectProperties(array, SerializationTag::kEndSparseJSArray)
.To(&num_properties) ||
!ReadVarint<uint32_t>().To(&expected_num_properties) ||
!ReadVarint<uint32_t>().To(&expected_length) ||
num_properties != expected_num_properties || length != expected_length) {
return MaybeHandle<JSArray>();
}
DCHECK(HasObjectWithID(id));
return scope.CloseAndEscape(array);
}
MaybeHandle<JSArray> ValueDeserializer::ReadDenseJSArray() {
// If we are at the end of the stack, abort. This function may recurse.
if (StackLimitCheck(isolate_).HasOverflowed()) return MaybeHandle<JSArray>();
uint32_t length;
if (!ReadVarint<uint32_t>().To(&length)) return MaybeHandle<JSArray>();
uint32_t id = next_id_++;
HandleScope scope(isolate_);
Handle<JSArray> array = isolate_->factory()->NewJSArray(
FAST_HOLEY_ELEMENTS, length, length, INITIALIZE_ARRAY_ELEMENTS_WITH_HOLE);
AddObjectWithID(id, array);
Handle<FixedArray> elements(FixedArray::cast(array->elements()), isolate_);
for (uint32_t i = 0; i < length; i++) {
Handle<Object> element;
if (!ReadObject().ToHandle(&element)) return MaybeHandle<JSArray>();
// TODO(jbroman): Distinguish between undefined and a hole.
if (element->IsUndefined(isolate_)) continue;
elements->set(i, *element);
}
uint32_t num_properties;
uint32_t expected_num_properties;
uint32_t expected_length;
if (!ReadJSObjectProperties(array, SerializationTag::kEndDenseJSArray)
.To(&num_properties) ||
!ReadVarint<uint32_t>().To(&expected_num_properties) ||
!ReadVarint<uint32_t>().To(&expected_length) ||
num_properties != expected_num_properties || length != expected_length) {
return MaybeHandle<JSArray>();
}
DCHECK(HasObjectWithID(id));
return scope.CloseAndEscape(array);
}
Maybe<uint32_t> ValueDeserializer::ReadJSObjectProperties(
Handle<JSObject> object, SerializationTag end_tag) {
for (uint32_t num_properties = 0;; num_properties++) {

View File

@ -72,6 +72,7 @@ class ValueSerializer {
void WriteString(Handle<String> string);
Maybe<bool> WriteJSReceiver(Handle<JSReceiver> receiver) WARN_UNUSED_RESULT;
Maybe<bool> WriteJSObject(Handle<JSObject> object) WARN_UNUSED_RESULT;
Maybe<bool> WriteJSArray(Handle<JSArray> array) WARN_UNUSED_RESULT;
/*
* Reads the specified keys from the object and writes key-value pairs to the
@ -140,6 +141,8 @@ class ValueDeserializer {
MaybeHandle<String> ReadUtf8String() WARN_UNUSED_RESULT;
MaybeHandle<String> ReadTwoByteString() WARN_UNUSED_RESULT;
MaybeHandle<JSObject> ReadJSObject() WARN_UNUSED_RESULT;
MaybeHandle<JSArray> ReadSparseJSArray() WARN_UNUSED_RESULT;
MaybeHandle<JSArray> ReadDenseJSArray() WARN_UNUSED_RESULT;
/*
* Reads key-value pairs into the object until the specified end tag is

View File

@ -707,5 +707,337 @@ TEST_F(ValueSerializerTest, DecodeDictionaryObjectVersion0) {
});
}
TEST_F(ValueSerializerTest, RoundTripArray) {
// A simple array of integers.
RoundTripTest("[1, 2, 3, 4, 5]", [this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
EXPECT_EQ(5, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Object.getPrototypeOf(result) === Array.prototype"));
EXPECT_TRUE(
EvaluateScriptForResultBool("result.toString() === '1,2,3,4,5'"));
});
// A long (sparse) array.
RoundTripTest(
"(() => { var x = new Array(1000); x[500] = 42; return x; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
EXPECT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[500] === 42"));
});
// Duplicate reference.
RoundTripTest(
"(() => { var y = {}; return [y, y]; })()", [this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result[1]"));
});
// Duplicate reference in a sparse array.
RoundTripTest(
"(() => { var x = new Array(1000); x[1] = x[500] = {}; return x; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[1] === 'object'"));
EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === result[500]"));
});
// Self reference.
RoundTripTest(
"(() => { var y = []; y[0] = y; return y; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result"));
});
// Self reference in a sparse array.
RoundTripTest(
"(() => { var y = new Array(1000); y[519] = y; return y; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[519] === result"));
});
// Array with additional properties.
RoundTripTest(
"(() => { var y = [1, 2]; y.foo = 'bar'; return y; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result.toString() === '1,2'"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'"));
});
// Sparse array with additional properties.
RoundTripTest(
"(() => { var y = new Array(1000); y.foo = 'bar'; return y; })()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool(
"result.toString() === ','.repeat(999)"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'"));
});
// The distinction between holes and undefined elements must be maintained.
RoundTripTest("[,undefined]", [this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[0] === 'undefined'"));
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[1] === 'undefined'"));
EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(0)"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.hasOwnProperty(1)"));
});
}
TEST_F(ValueSerializerTest, DecodeArray) {
// A simple array of integers.
DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x41, 0x05, 0x3f, 0x01, 0x49, 0x02,
0x3f, 0x01, 0x49, 0x04, 0x3f, 0x01, 0x49, 0x06, 0x3f, 0x01,
0x49, 0x08, 0x3f, 0x01, 0x49, 0x0a, 0x24, 0x00, 0x05, 0x00},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
EXPECT_EQ(5, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool(
"Object.getPrototypeOf(result) === Array.prototype"));
EXPECT_TRUE(EvaluateScriptForResultBool(
"result.toString() === '1,2,3,4,5'"));
});
// A long (sparse) array.
DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01, 0x49,
0xe8, 0x07, 0x3f, 0x01, 0x49, 0x54, 0x40, 0x01, 0xe8, 0x07},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
EXPECT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[500] === 42"));
});
// Duplicate reference.
DecodeTest(
{0xff, 0x09, 0x3f, 0x00, 0x41, 0x02, 0x3f, 0x01, 0x6f, 0x7b, 0x00, 0x3f,
0x02, 0x5e, 0x01, 0x24, 0x00, 0x02},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result[1]"));
});
// Duplicate reference in a sparse array.
DecodeTest(
{0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01, 0x49,
0x02, 0x3f, 0x01, 0x6f, 0x7b, 0x00, 0x3f, 0x02, 0x49, 0xe8,
0x07, 0x3f, 0x02, 0x5e, 0x01, 0x40, 0x02, 0xe8, 0x07, 0x00},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[1] === 'object'"));
EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === result[500]"));
});
// Self reference.
DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x41, 0x01, 0x3f, 0x01, 0x5e, 0x00, 0x24,
0x00, 0x01, 0x00},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === result"));
});
// Self reference in a sparse array.
DecodeTest(
{0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01, 0x49,
0x8e, 0x08, 0x3f, 0x01, 0x5e, 0x00, 0x40, 0x01, 0xe8, 0x07},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[519] === result"));
});
// Array with additional properties.
DecodeTest(
{0xff, 0x09, 0x3f, 0x00, 0x41, 0x02, 0x3f, 0x01, 0x49, 0x02, 0x3f,
0x01, 0x49, 0x04, 0x3f, 0x01, 0x53, 0x03, 0x66, 0x6f, 0x6f, 0x3f,
0x01, 0x53, 0x03, 0x62, 0x61, 0x72, 0x24, 0x01, 0x02, 0x00},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result.toString() === '1,2'"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'"));
});
// Sparse array with additional properties.
DecodeTest({0xff, 0x09, 0x3f, 0x00, 0x61, 0xe8, 0x07, 0x3f, 0x01,
0x53, 0x03, 0x66, 0x6f, 0x6f, 0x3f, 0x01, 0x53, 0x03,
0x62, 0x61, 0x72, 0x40, 0x01, 0xe8, 0x07, 0x00},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool(
"result.toString() === ','.repeat(999)"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.foo === 'bar'"));
});
// The distinction between holes and undefined elements must be maintained.
// Note that since the previous output from Chrome fails this test, an
// encoding using the sparse format was constructed instead.
DecodeTest(
{0xff, 0x09, 0x61, 0x02, 0x49, 0x02, 0x5f, 0x40, 0x01, 0x02},
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[0] === 'undefined'"));
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[1] === 'undefined'"));
EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(0)"));
EXPECT_TRUE(EvaluateScriptForResultBool("result.hasOwnProperty(1)"));
});
}
TEST_F(ValueSerializerTest, RoundTripArrayWithNonEnumerableElement) {
// Even though this array looks like [1,5,3], the 5 should be missing from the
// perspective of structured clone, which only clones properties that were
// enumerable.
RoundTripTest(
"(() => {"
" var x = [1,2,3];"
" Object.defineProperty(x, '1', {enumerable:false, value:5});"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(3, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty('1')"));
});
}
TEST_F(ValueSerializerTest, RoundTripArrayWithTrickyGetters) {
// If an element is deleted before it is serialized, then it's deleted.
RoundTripTest(
"(() => {"
" var x = [{ get a() { delete x[1]; }}, 42];"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[1] === 'undefined'"));
EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(1)"));
});
// Same for sparse arrays.
RoundTripTest(
"(() => {"
" var x = [{ get a() { delete x[1]; }}, 42];"
" x.length = 1000;"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(
EvaluateScriptForResultBool("typeof result[1] === 'undefined'"));
EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(1)"));
});
// If the length is changed, then the resulting array still has the original
// length, but elements that were not yet serialized are gone.
RoundTripTest(
"(() => {"
" var x = [1, { get a() { x.length = 0; }}, 3, 4];"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(4, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(2)"));
});
// Same for sparse arrays.
RoundTripTest(
"(() => {"
" var x = [1, { get a() { x.length = 0; }}, 3, 4];"
" x.length = 1000;"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[0] === 1"));
EXPECT_TRUE(EvaluateScriptForResultBool("!result.hasOwnProperty(2)"));
});
// If a getter makes a property non-enumerable, it should still be enumerated
// as enumeration happens once before getters are invoked.
RoundTripTest(
"(() => {"
" var x = [{ get a() {"
" Object.defineProperty(x, '1', { value: 3, enumerable: false });"
" }}, 2];"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 3"));
});
// Same for sparse arrays.
RoundTripTest(
"(() => {"
" var x = [{ get a() {"
" Object.defineProperty(x, '1', { value: 3, enumerable: false });"
" }}, 2];"
" x.length = 1000;"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 3"));
});
// Getters on the array itself must also run.
RoundTripTest(
"(() => {"
" var x = [1, 2, 3];"
" Object.defineProperty(x, '1', { enumerable: true, get: () => 4 });"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(3, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 4"));
});
// Same for sparse arrays.
RoundTripTest(
"(() => {"
" var x = [1, 2, 3];"
" Object.defineProperty(x, '1', { enumerable: true, get: () => 4 });"
" x.length = 1000;"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("result[1] === 4"));
});
// Even with a getter that deletes things, we don't read from the prototype.
RoundTripTest(
"(() => {"
" var x = [{ get a() { delete x[1]; } }, 2];"
" x.__proto__ = Object.create(Array.prototype, { 1: { value: 6 } });"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(2, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("!(1 in result)"));
});
// Same for sparse arrays.
RoundTripTest(
"(() => {"
" var x = [{ get a() { delete x[1]; } }, 2];"
" x.__proto__ = Object.create(Array.prototype, { 1: { value: 6 } });"
" x.length = 1000;"
" return x;"
"})()",
[this](Local<Value> value) {
ASSERT_TRUE(value->IsArray());
ASSERT_EQ(1000, Array::Cast(*value)->Length());
EXPECT_TRUE(EvaluateScriptForResultBool("!(1 in result)"));
});
}
} // namespace
} // namespace v8