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