Skip to content
File

Blob: src/workerd/jsg/function-test.c++

11.7 KB
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#include "jsg-test.h"
6 
7namespace workerd::jsg::test {
8namespace {
9 
10V8System v8System;
11class ContextGlobalObject: public Object, public ContextGlobal {};
12 
13struct CallbackContext: public ContextGlobalObject {
14 kj::String callCallback(Lock& js, jsg::Function<kj::String(kj::StringPtr, double)> function) {
15 return kj::str(function(js, "foo", 123), ", abc");
16 }
17 double callCallbackReturningBox(Lock& js, jsg::Function<Ref<NumberBox>()> function) {
18 return function(js)->value;
19 }
20 
21 struct Frobber {
22 kj::String s;
23 double n;
24 jsg::Function<kj::String(double)> frob;
25 Optional<jsg::Function<kj::String(double)>> optionalFrob;
26 kj::Maybe<jsg::Function<kj::String(double)>> maybeFrob;
27 JSG_STRUCT(s, n, frob, optionalFrob, maybeFrob);
28 };
29 
30 kj::String callConstructor(Lock& js, Constructor<Frobber(kj::StringPtr, double)> constructor) {
31 auto frobber = constructor(js, "foo", 123);
32 return kj::str(frobber.s, frobber.n, frobber.frob(js, 321),
33 KJ_ASSERT_NONNULL(frobber.optionalFrob)(js, 654),
34 KJ_ASSERT_NONNULL(frobber.maybeFrob)(js, 987));
35 }
36 
37 JSG_RESOURCE_TYPE(CallbackContext) {
38 JSG_NESTED_TYPE(NumberBox);
39 JSG_METHOD(callCallback);
40 JSG_METHOD(callCallbackReturningBox);
41 JSG_METHOD(callConstructor);
42 }
43};
44JSG_DECLARE_ISOLATE_TYPE(CallbackIsolate, CallbackContext, CallbackContext::Frobber, NumberBox);
45 
46KJ_TEST("callbacks") {
47 Evaluator<CallbackContext, CallbackIsolate> e(v8System);
48 e.expectEval("callCallback((str, num) => {\n"
49 " return [typeof str, str, typeof num, num.toString(), 'bar'].join(', ');\n"
50 "})",
51 "string", "string, foo, number, 123, bar, abc");
52 
53 e.expectEval("callCallback((str, num) => {\n"
54 " throw new Error('error message')\n"
55 "})",
56 "throws", "Error: error message");
57 
58 e.expectEval("callCallbackReturningBox(() => {\n"
59 " return new NumberBox(123);\n"
60 "})",
61 "number", "123");
62 e.expectEval("callCallbackReturningBox(() => {\n"
63 " return 'foo';\n"
64 "})",
65 "throws", "TypeError: Callback returned incorrect type; expected 'NumberBox'");
66 
67 e.expectEval("class Frobber {\n"
68 " constructor(s, n) {\n"
69 " this.s = s;\n"
70 " this.n = n;\n"
71 " }\n"
72 " frob(m) {\n"
73 " return this.s + (m + this.n);\n"
74 " }\n"
75 " optionalFrob(m) {\n"
76 " return 'opn' + this.s + (m + this.n);\n"
77 " }\n"
78 " maybeFrob(m) {\n"
79 " return 'mby' + this.s + (m + this.n);\n"
80 " }\n"
81 "}\n"
82 "callConstructor(Frobber)",
83 "string", "foo123foo444opnfoo777mbyfoo1110");
84}
85 
86// ========================================================================================
87 
88struct WrapContext: public ContextGlobalObject {
89 auto returnFunction(double value) {
90 return [value](Lock&, double value2) { return value + value2; };
91 }
92 auto returnFunctionWithInfo(double value) {
93 return [value](Lock&, const v8::FunctionCallbackInfo<v8::Value>& info, double value2) {
94 // Prove that we received `info` by adding in the argument count.
95 return value + value2 + info.Length();
96 };
97 }
98 auto returnFunctionMutable(double value) {
99 return [value](Lock&, double value2) mutable { return value + value2; };
100 }
101 auto returnFunctionWithInfoMutable(double value) {
102 return [value](Lock&, const v8::FunctionCallbackInfo<v8::Value>& info, double value2) mutable {
103 // Prove that we received `info` by adding in the argument count.
104 return value + value2 + info.Length();
105 };
106 }
107 auto returnFunctionReturningVoid(double value) {
108 return [value](Lock&, NumberBox& box) -> void { box.value = value; };
109 }
110 
111 JSG_RESOURCE_TYPE(WrapContext) {
112 JSG_NESTED_TYPE(NumberBox);
113 JSG_METHOD(returnFunction);
114 JSG_METHOD(returnFunctionWithInfo);
115 JSG_METHOD(returnFunctionMutable);
116 JSG_METHOD(returnFunctionWithInfoMutable);
117 JSG_METHOD(returnFunctionReturningVoid);
118 }
119};
120JSG_DECLARE_ISOLATE_TYPE(WrapIsolate, WrapContext, NumberBox);
121 
122KJ_TEST("wrap functions") {
123 Evaluator<WrapContext, WrapIsolate> e(v8System);
124 
125 e.expectEval("returnFunction(123)(321)", "number", "444");
126 e.expectEval("returnFunctionWithInfo(123)(321, '', undefined)", "number", "447");
127 e.expectEval("returnFunctionMutable(123)(321)", "number", "444");
128 e.expectEval("returnFunctionWithInfoMutable(123)(321, '', undefined)", "number", "447");
129 
130 e.expectEval("var nb = new NumberBox(321);\n"
131 "var ret = returnFunctionReturningVoid(123)(nb);\n"
132 "ret === undefined ? nb.value : 555",
133 "number", "123");
134}
135 
136// ========================================================================================
137 
138struct FunctionContext: public ContextGlobalObject {
139 auto test(Lock& js, Function<bool(int)> fn) {
140 return fn(js, 1);
141 }
142 
143 struct Foo {
144 Function<bool()> fn;
145 JSG_STRUCT(fn);
146 };
147 
148 auto test2(Lock& js, Foo foo) {
149 return foo.fn(js);
150 }
151 
152 jsg::Function<double(double)> getSquare(Lock& js) {
153 jsg::Function<double(double)> result = [](Lock&, double x) { return x * x; };
154 
155 // Check we can call it directly.
156 KJ_ASSERT(result(js, 11) == 121);
157 
158 return result;
159 }
160 
161 struct VisitDetector {
162 bool visited = false;
163 void visitForGc(GcVisitor& visitor) {
164 visited = true;
165 }
166 };
167 
168 jsg::Function<int(int)> getGcLambda() {
169 return JSG_VISITABLE_LAMBDA((v1 = VisitDetector(), v2 = VisitDetector(), v3 = VisitDetector()),
170 (v1, v3), (Lock&, int i) {
171 KJ_ASSERT(i == 123);
172 
173 // Should return 5, since v1 and v3 are visited but v2 is not. Note that a discovery
174 // visitation pass happens immediately upon constructing wrappers -- we don't need to wait
175 // for an actual GC pass, which is nice for this test.
176 return v1.visited + v2.visited * 2 + v3.visited * 4;
177 });
178 }
179 
180 jsg::Function<int(int, int)> getTwoArgs() {
181 return JSG_VISITABLE_LAMBDA((), (), (Lock&, int i, int j) {
182 // Also test an unparenthesized comma...
183 return ++i, i * j;
184 });
185 }
186 
187 kj::String testTryCatch(Lock& js, jsg::Function<int()> thrower) {
188 return js.tryCatch([&]() { return kj::str(thrower(js)); }, [&](Value exception) {
189 auto handle = exception.getHandle(js);
190 return kj::str("caught: ", handle);
191 });
192 }
193 
194 kj::String testTryCatch2(Lock& js, jsg::Function<int()> thrower) {
195 // Here we prove that the macro is if-else friendly.
196 if (true) JSG_TRY(js) {
197 return kj::str(thrower(js));
198 }
199 JSG_CATCH(exception) {
200 auto handle = exception.getHandle(js);
201 return kj::str("caught: ", handle);
202 }
203 else {
204 KJ_UNREACHABLE;
205 }
206 }
207 
208 kj::String testTryCatchWithOptions(Lock& js, jsg::Function<void()> thrower) {
209 // Test that JSG_CATCH can accept ExceptionToJsOptions.
210 JSG_TRY(js) {
211 thrower(js);
212 return kj::str("no exception");
213 }
214 JSG_CATCH(exception, {.ignoreDetail = true}) {
215 auto handle = exception.getHandle(js);
216 return kj::str("caught with options: ", handle);
217 }
218 }
219 
220 kj::String testNestedTryCatchInnerCatches(Lock& js, jsg::Function<void()> thrower) {
221 // Test nested JSG_TRY/JSG_CATCH where inner catches, outer doesn't see exception.
222 JSG_TRY(js) {
223 kj::String innerResult;
224 JSG_TRY(js) {
225 thrower(js);
226 innerResult = kj::str("inner: no exception");
227 }
228 JSG_CATCH(innerException) {
229 innerResult = kj::str("inner caught: ", innerException.getHandle(js));
230 }
231 return kj::str("outer: no exception, ", innerResult);
232 }
233 JSG_CATCH(outerException) {
234 return kj::str("outer caught: ", outerException.getHandle(js));
235 }
236 }
237 
238 kj::String testNestedTryCatchOuterCatches(Lock& js, jsg::Function<void()> thrower) {
239 // Test nested JSG_TRY/JSG_CATCH where inner rethrows, outer catches.
240 JSG_TRY(js) {
241 JSG_TRY(js) {
242 thrower(js);
243 return kj::str("inner: no exception");
244 }
245 JSG_CATCH(innerException) {
246 // Rethrow so outer can catch
247 js.throwException(kj::mv(innerException));
248 }
249 return kj::str("outer: no exception");
250 }
251 JSG_CATCH(outerException) {
252 return kj::str("outer caught: ", outerException.getHandle(js));
253 }
254 }
255 
256 JSG_RESOURCE_TYPE(FunctionContext) {
257 JSG_METHOD(test);
258 JSG_METHOD(test2);
259 JSG_METHOD(testTryCatch);
260 JSG_METHOD(testTryCatch2);
261 JSG_METHOD(testTryCatchWithOptions);
262 JSG_METHOD(testNestedTryCatchInnerCatches);
263 JSG_METHOD(testNestedTryCatchOuterCatches);
264 
265 JSG_READONLY_PROTOTYPE_PROPERTY(square, getSquare);
266 JSG_READONLY_PROTOTYPE_PROPERTY(gcLambda, getGcLambda);
267 JSG_READONLY_PROTOTYPE_PROPERTY(twoArgs, getTwoArgs);
268 }
269};
270JSG_DECLARE_ISOLATE_TYPE(FunctionIsolate, FunctionContext, FunctionContext::Foo);
271 
272KJ_TEST("jsg::Function<T>") {
273 Evaluator<FunctionContext, FunctionIsolate> e(v8System);
274 
275 e.expectEval("test((val) => val === 1)", "boolean", "true");
276 
277 // This variation checks that a jsg::Function pulled off a struct properly
278 // preserves "this" as a reference to the object it was pulled off of.
279 e.expectEval("const m = { fn() { return this === m; } }; test2(m);", "boolean", "true");
280 
281 e.expectEval("square(5)", "number", "25");
282 
283 e.expectEval("gcLambda(123)", "number", "5");
284 
285 e.expectEval("twoArgs(2, 5)", "number", "15");
286 
287 e.expectEval("testTryCatch(() => { return 123; })", "string", "123");
288 e.expectEval("testTryCatch(() => { throw new Error('foo'); })", "string", "caught: Error: foo");
289 
290 e.expectEval("testTryCatch2(() => { return 123; })", "string", "123");
291 e.expectEval("testTryCatch2(() => { throw new Error('foo'); })", "string", "caught: Error: foo");
292 
293 e.expectEval("testTryCatchWithOptions(() => {})", "string", "no exception");
294 e.expectEval("testTryCatchWithOptions(() => { throw new Error('bar'); })", "string",
295 "caught with options: Error: bar");
296 
297 // Nested JSG_TRY/JSG_CATCH tests
298 e.expectEval("testNestedTryCatchInnerCatches(() => {})", "string",
299 "outer: no exception, inner: no exception");
300 e.expectEval("testNestedTryCatchInnerCatches(() => { throw new Error('inner'); })", "string",
301 "outer: no exception, inner caught: Error: inner");
302 
303 e.expectEval("testNestedTryCatchOuterCatches(() => {})", "string", "inner: no exception");
304 e.expectEval("testNestedTryCatchOuterCatches(() => { throw new Error('rethrown'); })", "string",
305 "outer caught: Error: rethrown");
306}
307 
308KJ_TEST("JSG_TRY/JSG_CATCH with TerminateExecution") {
309 Evaluator<FunctionContext, FunctionIsolate> e(v8System);
310 
311 // TerminateExecution should propagate through JSG_CATCH without being caught.
312 // The Evaluator's run() method will detect the termination and throw.
313 KJ_EXPECT_THROW_MESSAGE("TerminateExecution() was called", e.run([](auto& js) {
314 // Test single-level JSG_TRY/JSG_CATCH with TerminateExecution
315 JSG_TRY(js) {
316 js.terminateExecutionNow();
317 }
318 JSG_CATCH(exception) {
319 (void)exception;
320 KJ_FAIL_ASSERT("TerminateExecution was caught by JSG_CATCH");
321 }
322 }));
323 
324 KJ_EXPECT_THROW_MESSAGE("TerminateExecution() was called", e.run([](auto& js) {
325 // Test nested JSG_TRY/JSG_CATCH with TerminateExecution - should propagate through both
326 JSG_TRY(js) {
327 JSG_TRY(js) {
328 js.terminateExecutionNow();
329 }
330 JSG_CATCH(innerException) {
331 (void)innerException;
332 KJ_FAIL_ASSERT("TerminateExecution was caught by inner JSG_CATCH");
333 }
334 }
335 JSG_CATCH(outerException) {
336 (void)outerException;
337 KJ_FAIL_ASSERT("TerminateExecution was caught by outer JSG_CATCH");
338 }
339 }));
340}
341 
342} // namespace
343} // namespace workerd::jsg::test