Skip to content
File

Blob: src/workerd/jsg/jsg-test.h

cpp304 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#pragma once
6// Common JSG testing infrastructure
7 
8#include <workerd/jsg/jsg.h>
9#include <workerd/jsg/resource.h>
10#include <workerd/jsg/setup.h>
11#include <workerd/jsg/type-wrapper.h>
12 
13#include <kj/test.h>
14 
15namespace workerd::jsg::test {
16 
17// Checks the evaluation of a blob of JS code under the given context and isolate types.
18template <typename ContextType,
19 typename IsolateType,
20 typename ConfigurationType = decltype(nullptr),
21 // HACK: We allow passing another parameter here to set the template type of
22 // ModuleRegistryImpl correctly in expectEvalModule(). This type needs to be
23 // IsolateType##_TypeWrapper, but this is difficult to derive from the IsolateType
24 // typename and only a few tests use expectEvalModule(), so providing it is optional.
25 // Previously we always provided ContextType here, which causes a subtle UBSan/vptr
26 // violation.
27 typename IsolateType_TypeWrapper = ContextType>
28class Evaluator {
29 // TODO(cleanup): `ConfigurationType` currently can optionally be specified to fix the build
30 // in cases that the isolate includes types that require configuration, but currently the
31 // type is always default-constructed. What if you want to specify a test config?
32 public:
33 explicit Evaluator(V8System& v8System, ConfigurationType config = {})
34 : v8System(v8System),
35 config(config) {}
36 
37 IsolateType& getIsolate() {
38 // Slightly more efficient to only instantiate each isolate type once (17s vs. 20s):
39 static IsolateType isolate(
40 v8System, v8::IsolateGroup::GetDefault(), config, kj::heap<IsolateObserver>());
41 return isolate;
42 }
43 
44 void expectEvalModule(
45 kj::StringPtr code, kj::StringPtr expectedType, kj::StringPtr expectedValue) {
46 getIsolate().runInLockScope([&](IsolateType::Lock& lock) {
47 JSG_WITHIN_CONTEXT_SCOPE(lock,
48 lock.template newContext<ContextType>().getHandle(lock.v8Isolate), [&](jsg::Lock& js) {
49 // Compile code as "main" module
50 CompilationObserver observer;
51 auto modules = ModuleRegistryImpl<IsolateType_TypeWrapper>::from(js);
52 auto p = kj::Path::parse("main");
53 modules->add(p,
54 jsg::ModuleRegistry::ModuleInfo(lock, "main", code, nullptr /* compile cache */,
55 ModuleInfoCompileOption::BUNDLE, observer));
56 
57 // Instantiate the module
58 auto& moduleInfo = KJ_REQUIRE_NONNULL(modules->resolve(js, p));
59 auto module = moduleInfo.module.getHandle(js);
60 jsg::instantiateModule(js, module);
61 
62 // Module has to export "run" function
63 auto moduleNs = check(module->GetModuleNamespace()->ToObject(js.v8Context()));
64 auto runValue = check(moduleNs->Get(js.v8Context(), v8StrIntern(lock.v8Isolate, "run"_kj)));
65 
66 v8::TryCatch catcher(js.v8Isolate);
67 
68 // Run the function to get the result.
69 v8::Local<v8::Value> result;
70 if (v8::Function::Cast(*runValue)
71 ->Call(js.v8Context(), js.v8Context()->Global(), 0, nullptr)
72 .ToLocal(&result)) {
73 v8::String::Utf8Value type(js.v8Isolate, result->TypeOf(js.v8Isolate));
74 v8::String::Utf8Value value(js.v8Isolate, result);
75 
76 KJ_EXPECT(*type == expectedType, *type, expectedType);
77 KJ_EXPECT(*value == expectedValue, *value, expectedValue);
78 } else if (catcher.HasCaught()) {
79 v8::String::Utf8Value message(js.v8Isolate, catcher.Exception());
80 
81 KJ_EXPECT(expectedType == "throws", expectedType, catcher.Exception());
82 KJ_EXPECT(*message == expectedValue, *message, expectedValue);
83 } else {
84 KJ_FAIL_EXPECT("returned empty handle but didn't throw exception?");
85 }
86 });
87 });
88 }
89 
90 void expectEval(kj::StringPtr code, kj::StringPtr expectedType, kj::StringPtr expectedValue) {
91 getIsolate().runInLockScope([&](IsolateType::Lock& lock) {
92 JSG_WITHIN_CONTEXT_SCOPE(lock,
93 lock.template newContext<ContextType>().getHandle(lock.v8Isolate), [&](jsg::Lock& js) {
94 // Create a string containing the JavaScript source code.
95 v8::Local<v8::String> source = jsg::v8Str(js.v8Isolate, code);
96 
97 // Compile the source code.
98 v8::Local<v8::Script> script;
99 if (!v8::Script::Compile(js.v8Context(), source).ToLocal(&script)) {
100 KJ_FAIL_EXPECT("code didn't parse", code);
101 return;
102 }
103 
104 v8::TryCatch catcher(js.v8Isolate);
105 
106 // Run the script to get the result.
107 v8::Local<v8::Value> result;
108 if (script->Run(js.v8Context()).ToLocal(&result)) {
109 v8::String::Utf8Value type(js.v8Isolate, result->TypeOf(js.v8Isolate));
110 v8::String::Utf8Value value(js.v8Isolate, result);
111 
112 KJ_EXPECT(*type == expectedType, *type, expectedType);
113 KJ_EXPECT(*value == expectedValue, *value, expectedValue);
114 } else if (catcher.HasCaught()) {
115 v8::String::Utf8Value message(js.v8Isolate, catcher.Exception());
116 
117 KJ_EXPECT(expectedType == "throws", expectedType, catcher.Exception());
118 KJ_EXPECT(*message == expectedValue, *message, expectedValue);
119 } else {
120 KJ_FAIL_EXPECT("returned empty handle but didn't throw exception?");
121 }
122 });
123 });
124 }
125 
126 void setAllowEval(bool b) {
127 getIsolate().runInLockScope([&](IsolateType::Lock& lock) { lock.setAllowEval(b); });
128 }
129 
130 void setCaptureThrowsAsRejections(bool b) {
131 getIsolate().runInLockScope(
132 [&](IsolateType::Lock& lock) { lock.setCaptureThrowsAsRejections(b); });
133 }
134 
135 void runMicrotasks() {
136 getIsolate().runInLockScope([&](IsolateType::Lock& lock) { lock.runMicrotasks(); });
137 }
138 
139 void runMicrotasks(IsolateType::Lock& lock) {
140 lock.runMicrotasks();
141 }
142 
143 // Run some C++ code in a new lock and context.
144 template <typename Func>
145 void run(Func&& func) {
146 getIsolate().runInLockScope([&](IsolateType::Lock& lock) {
147 JSG_WITHIN_CONTEXT_SCOPE(lock,
148 lock.template newContext<ContextType>().getHandle(lock.v8Isolate), [&](jsg::Lock& js) {
149 v8::TryCatch tryCatch(js.v8Isolate);
150 
151 try {
152 func(lock);
153 } catch (JsExceptionThrown&) {
154 if (tryCatch.HasTerminated()) {
155 KJ_FAIL_ASSERT("TerminateExecution() was called");
156 } else {
157 KJ_ASSERT(tryCatch.HasCaught());
158 jsg::throwTunneledException(js.v8Isolate, tryCatch.Exception());
159 }
160 }
161 });
162 });
163 }
164 
165 private:
166 V8System& v8System;
167 ConfigurationType config;
168};
169 
170struct NumberBox: public Object {
171 double value;
172 
173 explicit NumberBox(double value): value(value) {}
174 NumberBox() = default;
175 
176 static Ref<NumberBox> constructor(jsg::Lock& js, double value) {
177 return js.alloc<NumberBox>(value);
178 }
179 
180 void increment() {
181 value += 1;
182 }
183 void incrementBy(double amount) {
184 value += amount;
185 }
186 void incrementByBox(NumberBox& amount) {
187 value += amount.value;
188 }
189 
190 double add(double other) {
191 return value + other;
192 }
193 double addBox(NumberBox& other) {
194 return value + other.value;
195 }
196 Ref<NumberBox> addReturnBox(jsg::Lock& js, double other) {
197 return js.alloc<NumberBox>(value + other);
198 }
199 double addMultiple(NumberBox& a, double b, NumberBox& c) {
200 return value + a.value + b + c.value;
201 }
202 
203 double getValue() {
204 return value;
205 }
206 void setValue(double newValue) {
207 value = newValue;
208 }
209 
210 Ref<NumberBox> getBoxed(jsg::Lock& js) {
211 return js.alloc<NumberBox>(value);
212 }
213 void setBoxed(NumberBox& newValue) {
214 value = newValue.value;
215 }
216 
217 v8::Local<v8::Value> getBoxedFromTypeHandler(
218 jsg::Lock& js, const TypeHandler<Ref<NumberBox>>& numberBoxTypeHandler) {
219 return numberBoxTypeHandler.wrap(js, js.alloc<NumberBox>(value));
220 }
221 
222 JSG_RESOURCE_TYPE(NumberBox) {
223 JSG_METHOD(increment);
224 JSG_METHOD(incrementBy);
225 JSG_METHOD(incrementByBox);
226 
227 JSG_METHOD(add);
228 JSG_METHOD(addBox);
229 JSG_METHOD(addReturnBox);
230 JSG_METHOD(addMultiple);
231 
232 JSG_METHOD(getValue);
233 JSG_METHOD(setValue);
234 
235 JSG_INSTANCE_PROPERTY(value, getValue, setValue);
236 JSG_INSTANCE_PROPERTY(boxed, getBoxed, setBoxed);
237 JSG_READONLY_INSTANCE_PROPERTY(boxedFromTypeHandler, getBoxedFromTypeHandler);
238 }
239};
240 
241class BoxBox: public Object {
242 public:
243 explicit BoxBox(Ref<NumberBox> inner): inner(kj::mv(inner)) {}
244 
245 Ref<NumberBox> inner;
246 
247 static Ref<BoxBox> constructor(jsg::Lock& js, NumberBox& inner, double add) {
248 return js.alloc<BoxBox>(js.alloc<NumberBox>(inner.value + add));
249 }
250 
251 Ref<NumberBox> getInner() {
252 return inner.addRef();
253 }
254 
255 JSG_RESOURCE_TYPE(BoxBox) {
256 JSG_READONLY_INSTANCE_PROPERTY(inner, getInner);
257 }
258 
259 private:
260 void visitForGc(GcVisitor& visitor) {
261 visitor.visit(inner);
262 }
263};
264 
265struct ExtendedNumberBox: public NumberBox {
266 static Ref<ExtendedNumberBox> constructor(jsg::Lock& js, double value, kj::String text) {
267 auto result = js.alloc<ExtendedNumberBox>();
268 result->value = value;
269 result->text = kj::mv(text);
270 return result;
271 }
272 
273 kj::StringPtr getText() {
274 return text;
275 }
276 void setText(kj::String newText) {
277 text = kj::mv(newText);
278 }
279 
280 kj::String text;
281 
282 JSG_RESOURCE_TYPE(ExtendedNumberBox) {
283 JSG_INHERIT(NumberBox);
284 
285 JSG_METHOD(getText);
286 JSG_METHOD(setText);
287 JSG_INSTANCE_PROPERTY(text, getText, setText);
288 }
289};
290 
291struct TestStruct {
292 kj::String str;
293 double num;
294 Ref<NumberBox> box;
295 
296 JSG_STRUCT(str, num, box);
297 
298 void visitForGc(GcVisitor& visitor) {
299 visitor.visit(box);
300 }
301};
302 
303} // namespace workerd::jsg::test