cf92f3c3745a26833d8cf720fa3b4b2b3d759c0a
[WebKit-https.git] / Source / JavaScriptCore / API / tests / testapi.cpp
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
40 extern "C" int testCAPIViaCpp(const char* filter);
41
42 class APIString {
43     WTF_MAKE_NONCOPYABLE(APIString);
44 public:
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
58 private:
59     JSStringRef m_string;
60 };
61
62 class APIContext {
63     WTF_MAKE_NONCOPYABLE(APIContext);
64 public:
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
90 private:
91     JSGlobalContextRef m_context;
92 };
93
94 template<typename T>
95 class APIVector : protected Vector<T> {
96     using Base = Vector<T>;
97 public:
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
122 private:
123     APIContext& m_context;
124 };
125
126 class TestAPI {
127 public:
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
141 private:
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
165 TestAPI::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
176 template<typename... ArgumentTypes>
177 TestAPI::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
199 template<typename... ArgumentTypes>
200 bool 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
209 template<typename... Strings>
210 bool 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
221 template<typename JSFunctor, typename APIFunctor>
222 void 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
236 APIVector<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
253 APIVector<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
269 static const char* isSymbolFunction = "(function isSymbol(symbol) { return typeof(symbol) === 'symbol'; })";
270 static const char* getFunction = "(function get(object, key) { return object[key]; })";
271 static const char* setFunction = "(function set(object, key, value) { object[key] = value; })";
272
273 void 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
281 void 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
288 void 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
307 void 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
343 void 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
374 void 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
404 void 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
437 void 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
481 int 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                             return;
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 }