1/*
2 * Copyright (C) 2017 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27
28#include "APICast.h"
29#include "JSCJSValueInlines.h"
30#include "JSObject.h"
31
32#include <JavaScriptCore/JSObjectRefPrivate.h>
33#include <JavaScriptCore/JavaScript.h>
34#include <wtf/DataLog.h>
35#include <wtf/Expected.h>
36#include <wtf/Noncopyable.h>
37#include <wtf/NumberOfCores.h>
38#include <wtf/Vector.h>
39
40extern "C" int testCAPIViaCpp(const char* filter);
41
42class APIString {
43 WTF_MAKE_NONCOPYABLE(APIString);
44public:
45
46 APIString(const char* string)
47 : m_string(JSStringCreateWithUTF8CString(string))
48 {
49 }
50
51 ~APIString()
52 {
53 JSStringRelease(m_string);
54 }
55
56 operator JSStringRef() { return m_string; }
57
58private:
59 JSStringRef m_string;
60};
61
62class APIContext {
63 WTF_MAKE_NONCOPYABLE(APIContext);
64public:
65
66 APIContext()
67 : m_context(JSGlobalContextCreate(nullptr))
68 {
69 APIString print("print");
70 JSObjectRef printFunction = JSObjectMakeFunctionWithCallback(m_context, print, [] (JSContextRef ctx, JSObjectRef, JSObjectRef, size_t argumentCount, const JSValueRef arguments[], JSValueRef*) {
71
72 JSC::ExecState* exec = toJS(ctx);
73 for (unsigned i = 0; i < argumentCount; i++)
74 dataLog(toJS(exec, arguments[i]));
75 dataLogLn();
76 return JSValueMakeUndefined(ctx);
77 });
78
79 JSObjectSetProperty(m_context, JSContextGetGlobalObject(m_context), print, printFunction, kJSPropertyAttributeNone, nullptr);
80 }
81
82 ~APIContext()
83 {
84 JSGlobalContextRelease(m_context);
85 }
86
87 operator JSGlobalContextRef() { return m_context; }
88 operator JSC::ExecState*() { return toJS(m_context); }
89
90private:
91 JSGlobalContextRef m_context;
92};
93
94template<typename T>
95class APIVector : protected Vector<T> {
96 using Base = Vector<T>;
97public:
98 APIVector(APIContext& context)
99 : Base()
100 , m_context(context)
101 {
102 }
103
104 ~APIVector()
105 {
106 for (auto& value : *this)
107 JSValueUnprotect(m_context, value);
108 }
109
110 using Vector<T>::operator[];
111 using Vector<T>::size;
112 using Vector<T>::begin;
113 using Vector<T>::end;
114 using typename Vector<T>::iterator;
115
116 void append(T value)
117 {
118 JSValueProtect(m_context, value);
119 Base::append(WTFMove(value));
120 }
121
122private:
123 APIContext& m_context;
124};
125
126class TestAPI {
127public:
128 int run(const char* filter);
129
130 void basicSymbol();
131 void symbolsTypeof();
132 void symbolsGetPropertyForKey();
133 void symbolsSetPropertyForKey();
134 void symbolsHasPropertyForKey();
135 void symbolsDeletePropertyForKey();
136 void promiseResolveTrue();
137 void promiseRejectTrue();
138
139 int failed() const { return m_failed; }
140
141private:
142
143 template<typename... Strings>
144 bool check(bool condition, Strings... message);
145
146 template<typename JSFunctor, typename APIFunctor>
147 void checkJSAndAPIMatch(const JSFunctor&, const APIFunctor&, const char* description);
148
149 // Helper methods.
150 using ScriptResult = Expected<JSValueRef, JSValueRef>;
151 ScriptResult evaluateScript(const char* script, JSObjectRef thisObject = nullptr);
152 template<typename... ArgumentTypes>
153 ScriptResult callFunction(const char* functionSource, ArgumentTypes... arguments);
154 template<typename... ArgumentTypes>
155 bool functionReturnsTrue(const char* functionSource, ArgumentTypes... arguments);
156
157 // Ways to make sets of interesting things.
158 APIVector<JSObjectRef> interestingObjects();
159 APIVector<JSValueRef> interestingKeys();
160
161 int m_failed { 0 };
162 APIContext context;
163};
164
165TestAPI::ScriptResult TestAPI::evaluateScript(const char* script, JSObjectRef thisObject)
166{
167 APIString scriptAPIString(script);
168 JSValueRef exception = nullptr;
169
170 JSValueRef result = JSEvaluateScript(context, scriptAPIString, thisObject, nullptr, 0, &exception);
171 if (exception)
172 return Unexpected<JSValueRef>(exception);
173 return ScriptResult(result);
174}
175
176template<typename... ArgumentTypes>
177TestAPI::ScriptResult TestAPI::callFunction(const char* functionSource, ArgumentTypes... arguments)
178{
179 JSValueRef function;
180 {
181 ScriptResult functionResult = evaluateScript(functionSource);
182 if (!functionResult)
183 return functionResult;
184 function = functionResult.value();
185 }
186
187 JSValueRef exception = nullptr;
188 if (JSObjectRef functionObject = JSValueToObject(context, function, &exception)) {
189 JSValueRef args[sizeof...(arguments)] { arguments... };
190 JSValueRef result = JSObjectCallAsFunction(context, functionObject, functionObject, sizeof...(arguments), args, &exception);
191 if (!exception)
192 return ScriptResult(result);
193 }
194
195 RELEASE_ASSERT(exception);
196 return Unexpected<JSValueRef>(exception);
197}
198
199template<typename... ArgumentTypes>
200bool TestAPI::functionReturnsTrue(const char* functionSource, ArgumentTypes... arguments)
201{
202 JSValueRef trueValue = JSValueMakeBoolean(context, true);
203 ScriptResult result = callFunction(functionSource, arguments...);
204 if (!result)
205 return false;
206 return JSValueIsStrictEqual(context, trueValue, result.value());
207}
208
209template<typename... Strings>
210bool TestAPI::check(bool condition, Strings... messages)
211{
212 if (!condition) {
213 dataLogLn(messages..., ": FAILED");
214 m_failed++;
215 } else
216 dataLogLn(messages..., ": PASSED");
217
218 return condition;
219}
220
221template<typename JSFunctor, typename APIFunctor>
222void TestAPI::checkJSAndAPIMatch(const JSFunctor& jsFunctor, const APIFunctor& apiFunctor, const char* description)
223{
224 JSValueRef exception = nullptr;
225 JSValueRef result = apiFunctor(&exception);
226 ScriptResult jsResult = jsFunctor();
227 if (!jsResult) {
228 check(exception, "JS and API calls should both throw an exception while ", description);
229 check(functionReturnsTrue("(function(a, b) { return a.constructor === b.constructor; })", exception, jsResult.error()), "JS and API calls should both throw the same exception while ", description);
230 } else {
231 check(!exception, "JS and API calls should both not throw an exception while ", description);
232 check(JSValueIsStrictEqual(context, result, jsResult.value()), "JS result and API calls should return the same value while ", description);
233 }
234}
235
236APIVector<JSObjectRef> TestAPI::interestingObjects()
237{
238 APIVector<JSObjectRef> result(context);
239 JSObjectRef array = JSValueToObject(context, evaluateScript(
240 "[{}, [], { [Symbol.iterator]: 1 }, new Date(), new String('str'), new Map(), new Set(), new WeakMap(), new WeakSet(), new Error(), new Number(42), new Boolean(), { get length() { throw new Error(); } }];").value(), nullptr);
241
242 APIString lengthString("length");
243 unsigned length = JSValueToNumber(context, JSObjectGetProperty(context, array, lengthString, nullptr), nullptr);
244 for (unsigned i = 0; i < length; i++) {
245 JSObjectRef object = JSValueToObject(context, JSObjectGetPropertyAtIndex(context, array, i, nullptr), nullptr);
246 ASSERT(object);
247 result.append(object);
248 }
249
250 return result;
251}
252
253APIVector<JSValueRef> TestAPI::interestingKeys()
254{
255 APIVector<JSValueRef> result(context);
256 JSObjectRef array = JSValueToObject(context, evaluateScript("[{}, [], 1, Symbol.iterator, 'length']").value(), nullptr);
257
258 APIString lengthString("length");
259 unsigned length = JSValueToNumber(context, JSObjectGetProperty(context, array, lengthString, nullptr), nullptr);
260 for (unsigned i = 0; i < length; i++) {
261 JSValueRef value = JSObjectGetPropertyAtIndex(context, array, i, nullptr);
262 ASSERT(value);
263 result.append(value);
264 }
265
266 return result;
267}
268
269static const char* isSymbolFunction = "(function isSymbol(symbol) { return typeof(symbol) === 'symbol'; })";
270static const char* getFunction = "(function get(object, key) { return object[key]; })";
271static const char* setFunction = "(function set(object, key, value) { object[key] = value; })";
272
273void TestAPI::basicSymbol()
274{
275 // Can't call Symbol as a constructor since it's not subclassable.
276 auto result = evaluateScript("Symbol('dope');");
277 check(JSValueGetType(context, result.value()) == kJSTypeSymbol, "dope get type is a symbol");
278 check(JSValueIsSymbol(context, result.value()), "dope is a symbol");
279}
280
281void TestAPI::symbolsTypeof()
282{
283 APIString description("dope");
284 JSValueRef symbol = JSValueMakeSymbol(context, description);
285 check(functionReturnsTrue(isSymbolFunction, symbol), "JSValueMakeSymbol makes a symbol value");
286}
287
288void TestAPI::symbolsGetPropertyForKey()
289{
290 auto objects = interestingObjects();
291 auto keys = interestingKeys();
292
293 for (auto& object : objects) {
294 dataLogLn("\nnext object: ", toJS(context, object));
295 for (auto& key : keys) {
296 dataLogLn("Using key: ", toJS(context, key));
297 checkJSAndAPIMatch(
298 [&] {
299 return callFunction(getFunction, object, key);
300 }, [&] (JSValueRef* exception) {
301 return JSObjectGetPropertyForKey(context, object, key, exception);
302 }, "checking get property keys");
303 }
304 }
305}
306
307void TestAPI::symbolsSetPropertyForKey()
308{
309 auto jsObjects = interestingObjects();
310 auto apiObjects = interestingObjects();
311 auto keys = interestingKeys();
312
313 JSValueRef theAnswer = JSValueMakeNumber(context, 42);
314 for (size_t i = 0; i < jsObjects.size(); i++) {
315 for (auto& key : keys) {
316 JSObjectRef jsObject = jsObjects[i];
317 JSObjectRef apiObject = apiObjects[i];
318 checkJSAndAPIMatch(
319 [&] {
320 return callFunction(setFunction, jsObject, key, theAnswer);
321 } , [&] (JSValueRef* exception) {
322 JSObjectSetPropertyForKey(context, apiObject, key, theAnswer, kJSPropertyAttributeNone, exception);
323 return JSValueMakeUndefined(context);
324 }, "setting property keys to the answer");
325 // Check get is the same on API object.
326 checkJSAndAPIMatch(
327 [&] {
328 return callFunction(getFunction, apiObject, key);
329 }, [&] (JSValueRef* exception) {
330 return JSObjectGetPropertyForKey(context, apiObject, key, exception);
331 }, "getting property keys from API objects");
332 // Check get is the same on respective objects.
333 checkJSAndAPIMatch(
334 [&] {
335 return callFunction(getFunction, jsObject, key);
336 }, [&] (JSValueRef* exception) {
337 return JSObjectGetPropertyForKey(context, apiObject, key, exception);
338 }, "getting property keys from respective objects");
339 }
340 }
341}
342
343void TestAPI::symbolsHasPropertyForKey()
344{
345 const char* hasFunction = "(function has(object, key) { return key in object; })";
346 auto objects = interestingObjects();
347 auto keys = interestingKeys();
348
349 JSValueRef theAnswer = JSValueMakeNumber(context, 42);
350 for (auto& object : objects) {
351 dataLogLn("\nNext object: ", toJS(context, object));
352 for (auto& key : keys) {
353 dataLogLn("Using key: ", toJS(context, key));
354 checkJSAndAPIMatch(
355 [&] {
356 return callFunction(hasFunction, object, key);
357 }, [&] (JSValueRef* exception) {
358 return JSValueMakeBoolean(context, JSObjectHasPropertyForKey(context, object, key, exception));
359 }, "checking has property keys unset");
360
361 check(!!callFunction(setFunction, object, key, theAnswer), "set property to the answer");
362
363 checkJSAndAPIMatch(
364 [&] {
365 return callFunction(hasFunction, object, key);
366 }, [&] (JSValueRef* exception) {
367 return JSValueMakeBoolean(context, JSObjectHasPropertyForKey(context, object, key, exception));
368 }, "checking has property keys set");
369 }
370 }
371}
372
373
374void TestAPI::symbolsDeletePropertyForKey()
375{
376 const char* deleteFunction = "(function del(object, key) { return delete object[key]; })";
377 auto objects = interestingObjects();
378 auto keys = interestingKeys();
379
380 JSValueRef theAnswer = JSValueMakeNumber(context, 42);
381 for (auto& object : objects) {
382 dataLogLn("\nNext object: ", toJS(context, object));
383 for (auto& key : keys) {
384 dataLogLn("Using key: ", toJS(context, key));
385 checkJSAndAPIMatch(
386 [&] {
387 return callFunction(deleteFunction, object, key);
388 }, [&] (JSValueRef* exception) {
389 return JSValueMakeBoolean(context, JSObjectDeletePropertyForKey(context, object, key, exception));
390 }, "checking has property keys unset");
391
392 check(!!callFunction(setFunction, object, key, theAnswer), "set property to the answer");
393
394 checkJSAndAPIMatch(
395 [&] {
396 return callFunction(deleteFunction, object, key);
397 }, [&] (JSValueRef* exception) {
398 return JSValueMakeBoolean(context, JSObjectDeletePropertyForKey(context, object, key, exception));
399 }, "checking has property keys set");
400 }
401 }
402}
403
404void TestAPI::promiseResolveTrue()
405{
406 JSObjectRef resolve;
407 JSObjectRef reject;
408 JSValueRef exception = nullptr;
409 JSObjectRef promise = JSObjectMakeDeferredPromise(context, &resolve, &reject, &exception);
410 check(!exception, "No exception should be thrown creating a deferred promise");
411
412 // Ugh, we don't have any C API that takes blocks... so we do this hack to capture the runner.
413 static TestAPI* tester = this;
414 static bool passedTrueCalled = false;
415
416 APIString trueString("passedTrue");
417 auto passedTrue = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t argumentCount, const JSValueRef arguments[], JSValueRef*) -> JSValueRef {
418 tester->check(argumentCount && JSValueIsStrictEqual(ctx, arguments[0], JSValueMakeBoolean(ctx, true)), "function should have been called back with true");
419 passedTrueCalled = true;
420 return JSValueMakeUndefined(ctx);
421 };
422
423 APIString thenString("then");
424 JSValueRef thenFunction = JSObjectGetProperty(context, promise, thenString, &exception);
425 check(!exception && thenFunction && JSValueIsObject(context, thenFunction), "Promise should have a then object property");
426
427 JSValueRef passedTrueFunction = JSObjectMakeFunctionWithCallback(context, trueString, passedTrue);
428 JSObjectCallAsFunction(context, const_cast<JSObjectRef>(thenFunction), promise, 1, &passedTrueFunction, &exception);
429 check(!exception, "No exception should be thrown setting up callback");
430
431 auto trueValue = JSValueMakeBoolean(context, true);
432 JSObjectCallAsFunction(context, resolve, resolve, 1, &trueValue, &exception);
433 check(!exception, "No exception should be thrown resolve promise");
434 check(passedTrueCalled, "then response function should have been called.");
435}
436
437void TestAPI::promiseRejectTrue()
438{
439 JSObjectRef resolve;
440 JSObjectRef reject;
441 JSValueRef exception = nullptr;
442 JSObjectRef promise = JSObjectMakeDeferredPromise(context, &resolve, &reject, &exception);
443 check(!exception, "No exception should be thrown creating a deferred promise");
444
445 // Ugh, we don't have any C API that takes blocks... so we do this hack to capture the runner.
446 static TestAPI* tester = this;
447 static bool passedTrueCalled = false;
448
449 APIString trueString("passedTrue");
450 auto passedTrue = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t argumentCount, const JSValueRef arguments[], JSValueRef*) -> JSValueRef {
451 tester->check(argumentCount && JSValueIsStrictEqual(ctx, arguments[0], JSValueMakeBoolean(ctx, true)), "function should have been called back with true");
452 passedTrueCalled = true;
453 return JSValueMakeUndefined(ctx);
454 };
455
456 APIString catchString("catch");
457 JSValueRef catchFunction = JSObjectGetProperty(context, promise, catchString, &exception);
458 check(!exception && catchFunction && JSValueIsObject(context, catchFunction), "Promise should have a then object property");
459
460 JSValueRef passedTrueFunction = JSObjectMakeFunctionWithCallback(context, trueString, passedTrue);
461 JSObjectCallAsFunction(context, const_cast<JSObjectRef>(catchFunction), promise, 1, &passedTrueFunction, &exception);
462 check(!exception, "No exception should be thrown setting up callback");
463
464 auto trueValue = JSValueMakeBoolean(context, true);
465 JSObjectCallAsFunction(context, reject, reject, 1, &trueValue, &exception);
466 check(!exception, "No exception should be thrown resolve promise");
467 check(passedTrueCalled, "then response function should have been called.");
468}
469
470#define RUN(test) do { \
471 if (!shouldRun(#test)) \
472 break; \
473 tasks.append( \
474 createSharedTask<void(TestAPI&)>( \
475 [&] (TestAPI& tester) { \
476 tester.test; \
477 dataLog(#test ": OK!\n"); \
478 })); \
479 } while (false)
480
481int testCAPIViaCpp(const char* filter)
482{
483 dataLogLn("Starting C-API tests in C++");
484
485 Deque<RefPtr<SharedTask<void(TestAPI&)>>> tasks;
486
487#if OS(DARWIN)
488 auto shouldRun = [&] (const char* testName) -> bool {
489 return !filter || !!strcasestr(testName, filter);
490 };
491#else
492 auto shouldRun = [] (const char*) -> bool { return true; };
493#endif
494
495 RUN(basicSymbol());
496 RUN(symbolsTypeof());
497 RUN(symbolsGetPropertyForKey());
498 RUN(symbolsSetPropertyForKey());
499 RUN(symbolsHasPropertyForKey());
500 RUN(symbolsDeletePropertyForKey());
501 RUN(promiseResolveTrue());
502 RUN(promiseRejectTrue());
503
504 if (tasks.isEmpty()) {
505 dataLogLn("Filtered all tests: ERROR");
506 return 1;
507 }
508
509 Lock lock;
510
511 static Atomic<int> failed { 0 };
512 Vector<Ref<Thread>> threads;
513 for (unsigned i = filter ? 1 : WTF::numberOfProcessorCores(); i--;) {
514 threads.append(Thread::create(
515 "Testapi via C++ thread",
516 [&] () {
517 TestAPI tester;
518 for (;;) {
519 RefPtr<SharedTask<void(TestAPI&)>> task;
520 {
521 LockHolder locker(lock);
522 if (tasks.isEmpty())
523 break;
524 task = tasks.takeFirst();
525 }
526
527 task->run(tester);
528 }
529 failed.exchangeAdd(tester.failed());
530 }));
531 }
532
533 for (auto& thread : threads)
534 thread->waitForCompletion();
535
536 dataLogLn("C-API tests in C++ had ", failed.load(), " failures");
537 return failed.load();
538}
539