File
Blob: src/workerd/io/frankenvalue-test.c++
| 1 | #include "frankenvalue.h" |
| 2 | |
| 3 | #include <workerd/jsg/jsg-test.h> |
| 4 | |
| 5 | #include <capnp/message.h> |
| 6 | #include <kj/debug.h> |
| 7 | #include <kj/test.h> |
| 8 | |
| 9 | namespace workerd { |
| 10 | namespace { |
| 11 | |
| 12 | jsg::V8System v8System; |
| 13 | class ContextGlobalObject: public jsg::Object, public jsg::ContextGlobal {}; |
| 14 | |
| 15 | enum class TestSerializationTag { TEST_SERIALIZABLE }; |
| 16 | |
| 17 | struct TestCapEntry final: public Frankenvalue::CapTableEntry { |
| 18 | int value; |
| 19 | |
| 20 | TestCapEntry(int value): value(value) {} |
| 21 | kj::Own<CapTableEntry> clone() override { |
| 22 | return kj::heap<TestCapEntry>(value); |
| 23 | } |
| 24 | }; |
| 25 | |
| 26 | class TestSerializableCap: public jsg::Object { |
| 27 | public: |
| 28 | TestSerializableCap(int value): value(value) {} |
| 29 | |
| 30 | int toJSON() { |
| 31 | return value; |
| 32 | } |
| 33 | |
| 34 | JSG_RESOURCE_TYPE(TestSerializableCap) { |
| 35 | JSG_METHOD(toJSON); |
| 36 | } |
| 37 | |
| 38 | void serialize(jsg::Lock& js, jsg::Serializer& serializer) { |
| 39 | auto& handler = KJ_ASSERT_NONNULL(serializer.getExternalHandler()); |
| 40 | auto externalHandler = dynamic_cast<Frankenvalue::CapTableBuilder*>(&handler); |
| 41 | KJ_ASSERT(externalHandler != nullptr); |
| 42 | serializer.writeRawUint32(externalHandler->add(kj::heap<TestCapEntry>(value))); |
| 43 | } |
| 44 | |
| 45 | static jsg::Ref<TestSerializableCap> deserialize( |
| 46 | jsg::Lock& js, TestSerializationTag tag, jsg::Deserializer& deserializer) { |
| 47 | auto& handler = KJ_REQUIRE_NONNULL(deserializer.getExternalHandler()); |
| 48 | auto externalHandler = dynamic_cast<Frankenvalue::CapTableReader*>(&handler); |
| 49 | KJ_REQUIRE(externalHandler != nullptr); |
| 50 | |
| 51 | auto& cap = KJ_REQUIRE_NONNULL(externalHandler->get(deserializer.readRawUint32())); |
| 52 | auto typedCap = dynamic_cast<TestCapEntry*>(&cap); |
| 53 | KJ_REQUIRE(typedCap != nullptr); |
| 54 | |
| 55 | return js.alloc<TestSerializableCap>(typedCap->value); |
| 56 | } |
| 57 | |
| 58 | JSG_SERIALIZABLE(TestSerializationTag::TEST_SERIALIZABLE); |
| 59 | |
| 60 | private: |
| 61 | int value; |
| 62 | }; |
| 63 | |
| 64 | struct TestContext: public ContextGlobalObject { |
| 65 | JSG_RESOURCE_TYPE(TestContext) {} |
| 66 | }; |
| 67 | JSG_DECLARE_ISOLATE_TYPE(TestIsolate, TestContext, TestSerializableCap); |
| 68 | |
| 69 | KJ_TEST("Frankenvalue") { |
| 70 | jsg::test::Evaluator<TestContext, TestIsolate> e(v8System); |
| 71 | |
| 72 | // Silence compiler warning. |
| 73 | (void)TestSerializableCap::jsgSerializeOneway; |
| 74 | (void)TestSerializableCap::jsgSerializeOldTags; |
| 75 | |
| 76 | e.run([&](auto& lock) { |
| 77 | jsg::Lock& js = lock; |
| 78 | auto makeSerializableCap = [&](int i) { |
| 79 | return jsg::JsValue(lock.wrap(js.v8Context(), js.alloc<TestSerializableCap>(i))); |
| 80 | }; |
| 81 | |
| 82 | // Create a value based on JSON. |
| 83 | Frankenvalue value = Frankenvalue::fromJson(kj::str(R"({"baz": 321, "qux": "xyz"})"_kj)); |
| 84 | |
| 85 | // prop1 is empty. |
| 86 | value.setProperty(kj::str("prop1"), {}); |
| 87 | |
| 88 | // prop2 is a V8-serialized value. |
| 89 | value.setProperty(kj::str("prop2"), [&]() { |
| 90 | auto obj = js.obj(); |
| 91 | obj.set(js, "foo", js.num(123)); |
| 92 | obj.set(js, "bar", js.str("abc"_kj)); |
| 93 | obj.set(js, "baz", makeSerializableCap(234)); |
| 94 | obj.set(js, "qux", makeSerializableCap(345)); |
| 95 | |
| 96 | auto result = Frankenvalue::fromJs(js, obj); |
| 97 | |
| 98 | result.setProperty(kj::str("nested"), [&]() { |
| 99 | auto nested = js.obj(); |
| 100 | nested.set(js, "corge", js.num(222)); |
| 101 | nested.set(js, "grault", |
| 102 | jsg::JsValue(lock.wrap(js.v8Context(), js.alloc<TestSerializableCap>(333)))); |
| 103 | return Frankenvalue::fromJs(js, nested); |
| 104 | }()); |
| 105 | result.setProperty(kj::str("nested2"), [&]() { |
| 106 | auto nested = js.obj(); |
| 107 | nested.set(js, "garply", js.num(444)); |
| 108 | nested.set(js, "waldo", |
| 109 | jsg::JsValue(lock.wrap(js.v8Context(), js.alloc<TestSerializableCap>(555)))); |
| 110 | return Frankenvalue::fromJs(js, nested); |
| 111 | }()); |
| 112 | |
| 113 | return result; |
| 114 | }()); |
| 115 | |
| 116 | KJ_ASSERT(value.getCapTable().size() == 4); |
| 117 | KJ_EXPECT(kj::downcast<TestCapEntry>(*value.getCapTable()[0]).value == 234); |
| 118 | KJ_EXPECT(kj::downcast<TestCapEntry>(*value.getCapTable()[1]).value == 345); |
| 119 | KJ_EXPECT(kj::downcast<TestCapEntry>(*value.getCapTable()[2]).value == 333); |
| 120 | KJ_EXPECT(kj::downcast<TestCapEntry>(*value.getCapTable()[3]).value == 555); |
| 121 | |
| 122 | // Round trip through capnp. |
| 123 | { |
| 124 | capnp::MallocMessageBuilder message; |
| 125 | auto builder = message.initRoot<rpc::Frankenvalue>(); |
| 126 | value.toCapnp(builder); |
| 127 | |
| 128 | kj::Vector<kj::Own<Frankenvalue::CapTableEntry>> newCapTable; |
| 129 | for (auto& entry: value.getCapTable()) { |
| 130 | newCapTable.add(entry->clone()); |
| 131 | } |
| 132 | |
| 133 | value = Frankenvalue::fromCapnp(builder.asReader(), kj::mv(newCapTable)); |
| 134 | } |
| 135 | |
| 136 | // Use clone(). |
| 137 | value = value.clone(); |
| 138 | |
| 139 | // Back to JS, then JSON, then check that nothing was lost. |
| 140 | KJ_EXPECT(js.serializeJson(value.toJs(js)) == |
| 141 | R"({"baz":321,"qux":"xyz","prop1":{},"prop2":{"foo":123,"bar":"abc","baz":234,"qux":345)"_kj |
| 142 | R"(,"nested":{"corge":222,"grault":333},"nested2":{"garply":444,"waldo":555}}})"_kj); |
| 143 | }); |
| 144 | } |
| 145 | |
| 146 | } // namespace |
| 147 | } // namespace workerd |