test_quickjs.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. import concurrent.futures
  2. import json
  3. import unittest
  4. import quickjs
  5. class LoadModule(unittest.TestCase):
  6. def test_42(self):
  7. self.assertEqual(quickjs.test(), 42)
  8. class Context(unittest.TestCase):
  9. def setUp(self):
  10. self.context = quickjs.Context()
  11. def test_eval_int(self):
  12. self.assertEqual(self.context.eval("40 + 2"), 42)
  13. def test_eval_float(self):
  14. self.assertEqual(self.context.eval("40.0 + 2.0"), 42.0)
  15. def test_eval_str(self):
  16. self.assertEqual(self.context.eval("'4' + '2'"), "42")
  17. def test_eval_bool(self):
  18. self.assertEqual(self.context.eval("true || false"), True)
  19. self.assertEqual(self.context.eval("true && false"), False)
  20. def test_eval_null(self):
  21. self.assertIsNone(self.context.eval("null"))
  22. def test_eval_undefined(self):
  23. self.assertIsNone(self.context.eval("undefined"))
  24. def test_wrong_type(self):
  25. with self.assertRaises(TypeError):
  26. self.assertEqual(self.context.eval(1), 42)
  27. def test_context_between_calls(self):
  28. self.context.eval("x = 40; y = 2;")
  29. self.assertEqual(self.context.eval("x + y"), 42)
  30. def test_function(self):
  31. self.context.eval("""
  32. function special(x) {
  33. return 40 + x;
  34. }
  35. """)
  36. self.assertEqual(self.context.eval("special(2)"), 42)
  37. def test_get(self):
  38. self.context.eval("x = 42; y = 'foo';")
  39. self.assertEqual(self.context.get("x"), 42)
  40. self.assertEqual(self.context.get("y"), "foo")
  41. self.assertEqual(self.context.get("z"), None)
  42. def test_set(self):
  43. self.context.eval("x = 'overriden'")
  44. self.context.set("x", 42)
  45. self.context.set("y", "foo")
  46. self.assertTrue(self.context.eval("x == 42"))
  47. self.assertTrue(self.context.eval("y == 'foo'"))
  48. def test_module(self):
  49. self.context.module("""
  50. export function test() {
  51. return 42;
  52. }
  53. """)
  54. def test_error(self):
  55. with self.assertRaisesRegex(quickjs.JSException, "ReferenceError: 'missing' is not defined"):
  56. self.context.eval("missing + missing")
  57. def test_lifetime(self):
  58. def get_f():
  59. context = quickjs.Context()
  60. f = context.eval("""
  61. a = function(x) {
  62. return 40 + x;
  63. }
  64. """)
  65. return f
  66. f = get_f()
  67. self.assertTrue(f)
  68. # The context has left the scope after f. f needs to keep the context alive for the
  69. # its lifetime. Otherwise, we will get problems.
  70. def test_backtrace(self):
  71. try:
  72. self.context.eval("""
  73. function funcA(x) {
  74. x.a.b = 1;
  75. }
  76. function funcB(x) {
  77. funcA(x);
  78. }
  79. funcB({});
  80. """)
  81. except Exception as e:
  82. msg = str(e)
  83. else:
  84. self.fail("Expected exception.")
  85. self.assertIn("at funcA (<input>:3)\n", msg)
  86. self.assertIn("at funcB (<input>:6)\n", msg)
  87. def test_memory_limit(self):
  88. code = """
  89. (function() {
  90. let arr = [];
  91. for (let i = 0; i < 1000; ++i) {
  92. arr.push(i);
  93. }
  94. })();
  95. """
  96. self.context.eval(code)
  97. self.context.set_memory_limit(1000)
  98. with self.assertRaisesRegex(quickjs.JSException, "null"):
  99. self.context.eval(code)
  100. self.context.set_memory_limit(1000000)
  101. self.context.eval(code)
  102. def test_time_limit(self):
  103. code = """
  104. (function() {
  105. let arr = [];
  106. for (let i = 0; i < 100000; ++i) {
  107. arr.push(i);
  108. }
  109. return arr;
  110. })();
  111. """
  112. self.context.eval(code)
  113. self.context.set_time_limit(0)
  114. with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"):
  115. self.context.eval(code)
  116. self.context.set_time_limit(-1)
  117. self.context.eval(code)
  118. def test_memory_usage(self):
  119. self.assertIn("memory_used_size", self.context.memory().keys())
  120. def test_json_simple(self):
  121. self.assertEqual(self.context.parse_json("42"), 42)
  122. def test_json_error(self):
  123. with self.assertRaisesRegex(quickjs.JSException, "unexpected token"):
  124. self.context.parse_json("a b c")
  125. def test_execute_pending_job(self):
  126. self.context.eval("obj = {}")
  127. self.assertEqual(self.context.execute_pending_job(), False)
  128. self.context.eval("Promise.resolve().then(() => {obj.x = 1;})")
  129. self.assertEqual(self.context.execute_pending_job(), True)
  130. self.assertEqual(self.context.eval("obj.x"), 1)
  131. self.assertEqual(self.context.execute_pending_job(), False)
  132. class CallIntoPython(unittest.TestCase):
  133. def setUp(self):
  134. self.context = quickjs.Context()
  135. def test_make_function(self):
  136. self.context.add_callable("f", lambda x: x + 2)
  137. self.assertEqual(self.context.eval("f(40)"), 42)
  138. def test_make_two_functions(self):
  139. for i in range(10):
  140. self.context.add_callable("f", lambda x: i + x + 2)
  141. self.context.add_callable("g", lambda x: i + x + 40)
  142. f = self.context.get("f")
  143. g = self.context.get("g")
  144. self.assertEqual(f(40) - i, 42)
  145. self.assertEqual(g(2) - i, 42)
  146. self.assertEqual(self.context.eval("((f, a) => f(a))")(f, 40) - i, 42)
  147. def test_make_function_call_from_js(self):
  148. self.context.add_callable("f", lambda x: x + 2)
  149. g = self.context.eval("""(
  150. function() {
  151. return f(20) + 20;
  152. }
  153. )""")
  154. self.assertEqual(g(), 42)
  155. def test_python_function_raises(self):
  156. def error(a):
  157. raise ValueError("A")
  158. self.context.add_callable("error", error)
  159. with self.assertRaisesRegex(quickjs.JSException, "Python call failed"):
  160. self.context.eval("error(0)")
  161. def test_make_function_two_args(self):
  162. def concat(a, b):
  163. return a + b
  164. self.context.add_callable("concat", concat)
  165. result = self.context.eval("concat(40, 2)")
  166. self.assertEqual(result, 42)
  167. concat = self.context.get("concat")
  168. result = self.context.eval("((f, a, b) => 22 + f(a, b))")(concat, 10, 10)
  169. self.assertEqual(result, 42)
  170. def test_make_function_two_string_args(self):
  171. """Without the JS_DupValue in js_c_function, this test crashes."""
  172. def concat(a, b):
  173. return a + "-" + b
  174. self.context.add_callable("concat", concat)
  175. concat = self.context.get("concat")
  176. result = concat("aaa", "bbb")
  177. self.assertEqual(result, "aaa-bbb")
  178. def test_can_eval_in_same_context(self):
  179. self.context.add_callable("f", lambda: 40 + self.context.eval("1 + 1"))
  180. self.assertEqual(self.context.eval("f()"), 42)
  181. def test_can_call_in_same_context(self):
  182. inner = self.context.eval("(function() { return 42; })")
  183. self.context.add_callable("f", lambda: inner())
  184. self.assertEqual(self.context.eval("f()"), 42)
  185. def test_invalid_argument(self):
  186. self.context.add_callable("p", lambda: 42)
  187. self.assertEqual(self.context.eval("p()"), 42)
  188. with self.assertRaisesRegex(quickjs.JSException, "Python call failed"):
  189. self.context.eval("p(1)")
  190. with self.assertRaisesRegex(quickjs.JSException, "Python call failed"):
  191. self.context.eval("p({})")
  192. def test_time_limit_disallowed(self):
  193. self.context.add_callable("f", lambda x: x + 2)
  194. self.context.set_time_limit(1000)
  195. with self.assertRaises(quickjs.JSException):
  196. self.context.eval("f(40)")
  197. def test_conversion_failure_does_not_raise_system_error(self):
  198. # https://github.com/PetterS/quickjs/issues/38
  199. def test_list():
  200. return [1, 2, 3]
  201. self.context.add_callable("test_list", test_list)
  202. with self.assertRaises(quickjs.JSException):
  203. # With incorrect error handling, this (safely) made Python raise a SystemError
  204. # instead of a JS exception.
  205. self.context.eval("test_list()")
  206. class Object(unittest.TestCase):
  207. def setUp(self):
  208. self.context = quickjs.Context()
  209. def test_function_is_object(self):
  210. f = self.context.eval("""
  211. a = function(x) {
  212. return 40 + x;
  213. }
  214. """)
  215. self.assertIsInstance(f, quickjs.Object)
  216. def test_function_call_int(self):
  217. f = self.context.eval("""
  218. f = function(x) {
  219. return 40 + x;
  220. }
  221. """)
  222. self.assertEqual(f(2), 42)
  223. def test_function_call_int_two_args(self):
  224. f = self.context.eval("""
  225. f = function(x, y) {
  226. return 40 + x + y;
  227. }
  228. """)
  229. self.assertEqual(f(3, -1), 42)
  230. def test_function_call_many_times(self):
  231. n = 1000
  232. f = self.context.eval("""
  233. f = function(x, y) {
  234. return x + y;
  235. }
  236. """)
  237. s = 0
  238. for i in range(n):
  239. s += f(1, 1)
  240. self.assertEqual(s, 2 * n)
  241. def test_function_call_str(self):
  242. f = self.context.eval("""
  243. f = function(a) {
  244. return a + " hej";
  245. }
  246. """)
  247. self.assertEqual(f("1"), "1 hej")
  248. def test_function_call_str_three_args(self):
  249. f = self.context.eval("""
  250. f = function(a, b, c) {
  251. return a + " hej " + b + " ho " + c;
  252. }
  253. """)
  254. self.assertEqual(f("1", "2", "3"), "1 hej 2 ho 3")
  255. def test_function_call_object(self):
  256. d = self.context.eval("d = {data: 42};")
  257. f = self.context.eval("""
  258. f = function(d) {
  259. return d.data;
  260. }
  261. """)
  262. self.assertEqual(f(d), 42)
  263. # Try again to make sure refcounting works.
  264. self.assertEqual(f(d), 42)
  265. self.assertEqual(f(d), 42)
  266. def test_function_call_unsupported_arg(self):
  267. f = self.context.eval("""
  268. f = function(x) {
  269. return 40 + x;
  270. }
  271. """)
  272. with self.assertRaisesRegex(TypeError, "Unsupported type"):
  273. self.assertEqual(f({}), 42)
  274. def test_json(self):
  275. d = self.context.eval("d = {data: 42};")
  276. self.assertEqual(json.loads(d.json()), {"data": 42})
  277. def test_call_nonfunction(self):
  278. d = self.context.eval("({data: 42})")
  279. with self.assertRaisesRegex(quickjs.JSException, "TypeError: not a function"):
  280. d(1)
  281. def test_wrong_context(self):
  282. context1 = quickjs.Context()
  283. context2 = quickjs.Context()
  284. f = context1.eval("(function(x) { return x.a; })")
  285. d = context2.eval("({a: 1})")
  286. with self.assertRaisesRegex(ValueError, "Can not mix JS objects from different contexts."):
  287. f(d)
  288. def test_get(self):
  289. self.context.eval("a = {x: 42, y: 'foo'};")
  290. a = self.context.get_global().get("a")
  291. self.assertEqual(a.get("x"), 42)
  292. self.assertEqual(a.get("y"), "foo")
  293. self.assertEqual(a.get("z"), None)
  294. def test_set(self):
  295. self.context.eval("a = {x: 'overridden'}")
  296. a = self.context.get_global().get("a")
  297. a.set("x", 42)
  298. a.set("y", "foo")
  299. self.assertTrue(self.context.eval("a.x == 42"))
  300. self.assertTrue(self.context.eval("a.y == 'foo'"))
  301. def test_make_function(self):
  302. print(11)
  303. self.context.get_global().set("f", lambda x: x + 2)
  304. self.assertEqual(self.context.eval("f(40)"), 42)
  305. class FunctionTest(unittest.TestCase):
  306. def test_adder(self):
  307. f = quickjs.Function(
  308. "adder", """
  309. function adder(x, y) {
  310. return x + y;
  311. }
  312. """)
  313. self.assertEqual(f(1, 1), 2)
  314. self.assertEqual(f(100, 200), 300)
  315. self.assertEqual(f("a", "b"), "ab")
  316. def test_identity(self):
  317. identity = quickjs.Function(
  318. "identity", """
  319. function identity(x) {
  320. return x;
  321. }
  322. """)
  323. for x in [True, [1], {"a": 2}, 1, 1.5, "hej", None]:
  324. self.assertEqual(identity(x), x)
  325. def test_bool(self):
  326. f = quickjs.Function(
  327. "f", """
  328. function f(x) {
  329. return [typeof x ,!x];
  330. }
  331. """)
  332. self.assertEqual(f(False), ["boolean", True])
  333. self.assertEqual(f(True), ["boolean", False])
  334. def test_empty(self):
  335. f = quickjs.Function("f", "function f() { }")
  336. self.assertEqual(f(), None)
  337. def test_lists(self):
  338. f = quickjs.Function(
  339. "f", """
  340. function f(arr) {
  341. const result = [];
  342. arr.forEach(function(elem) {
  343. result.push(elem + 42);
  344. });
  345. return result;
  346. }""")
  347. self.assertEqual(f([0, 1, 2]), [42, 43, 44])
  348. def test_dict(self):
  349. f = quickjs.Function(
  350. "f", """
  351. function f(obj) {
  352. return obj.data;
  353. }""")
  354. self.assertEqual(f({"data": {"value": 42}}), {"value": 42})
  355. def test_time_limit(self):
  356. f = quickjs.Function(
  357. "f", """
  358. function f() {
  359. let arr = [];
  360. for (let i = 0; i < 100000; ++i) {
  361. arr.push(i);
  362. }
  363. return arr;
  364. }
  365. """)
  366. f()
  367. f.set_time_limit(0)
  368. with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"):
  369. f()
  370. f.set_time_limit(-1)
  371. f()
  372. def test_garbage_collection(self):
  373. f = quickjs.Function(
  374. "f", """
  375. function f() {
  376. let a = {};
  377. let b = {};
  378. a.b = b;
  379. b.a = a;
  380. a.i = 42;
  381. return a.i;
  382. }
  383. """)
  384. initial_count = f.memory()["obj_count"]
  385. for i in range(10):
  386. prev_count = f.memory()["obj_count"]
  387. self.assertEqual(f(run_gc=False), 42)
  388. current_count = f.memory()["obj_count"]
  389. self.assertGreater(current_count, prev_count)
  390. f.gc()
  391. self.assertLessEqual(f.memory()["obj_count"], initial_count)
  392. def test_deep_recursion(self):
  393. f = quickjs.Function(
  394. "f", """
  395. function f(v) {
  396. if (v <= 0) {
  397. return 0;
  398. } else {
  399. return 1 + f(v - 1);
  400. }
  401. }
  402. """)
  403. self.assertEqual(f(100), 100)
  404. limit = 500
  405. with self.assertRaises(quickjs.StackOverflow):
  406. f(limit)
  407. f.set_max_stack_size(2000 * limit)
  408. self.assertEqual(f(limit), limit)
  409. def test_add_callable(self):
  410. f = quickjs.Function(
  411. "f", """
  412. function f() {
  413. return pfunc();
  414. }
  415. """)
  416. f.add_callable("pfunc", lambda: 42)
  417. self.assertEqual(f(), 42)
  418. def test_execute_pending_job(self):
  419. f = quickjs.Function(
  420. "f", """
  421. obj = {x: 0, y: 0};
  422. async function a() {
  423. obj.x = await 1;
  424. }
  425. a();
  426. Promise.resolve().then(() => {obj.y = 1});
  427. function f() {
  428. return obj.x + obj.y;
  429. }
  430. """)
  431. self.assertEqual(f(), 0)
  432. self.assertEqual(f.execute_pending_job(), True)
  433. self.assertEqual(f(), 1)
  434. self.assertEqual(f.execute_pending_job(), True)
  435. self.assertEqual(f(), 2)
  436. self.assertEqual(f.execute_pending_job(), False)
  437. class JavascriptFeatures(unittest.TestCase):
  438. def test_unicode_strings(self):
  439. identity = quickjs.Function(
  440. "identity", """
  441. function identity(x) {
  442. return x;
  443. }
  444. """)
  445. context = quickjs.Context()
  446. for x in ["äpple", "≤≥", "☺"]:
  447. self.assertEqual(identity(x), x)
  448. self.assertEqual(context.eval('(function(){ return "' + x + '";})()'), x)
  449. def test_es2020_optional_chaining(self):
  450. f = quickjs.Function(
  451. "f", """
  452. function f(x) {
  453. return x?.one?.two;
  454. }
  455. """)
  456. self.assertIsNone(f({}))
  457. self.assertIsNone(f({"one": 12}))
  458. self.assertEqual(f({"one": {"two": 42}}), 42)
  459. def test_es2020_null_coalescing(self):
  460. f = quickjs.Function(
  461. "f", """
  462. function f(x) {
  463. return x ?? 42;
  464. }
  465. """)
  466. self.assertEqual(f(""), "")
  467. self.assertEqual(f(0), 0)
  468. self.assertEqual(f(11), 11)
  469. self.assertEqual(f(None), 42)
  470. def test_symbol_conversion(self):
  471. context = quickjs.Context()
  472. context.eval("a = Symbol();")
  473. context.set("b", context.eval("a"))
  474. self.assertTrue(context.eval("a === b"))
  475. def test_large_python_integers_to_quickjs(self):
  476. context = quickjs.Context()
  477. # Without a careful implementation, this made Python raise a SystemError/OverflowError.
  478. context.set("v", 10**25)
  479. # There is precision loss occurring in JS due to
  480. # the floating point implementation of numbers.
  481. self.assertTrue(context.eval("v == 1e25"))
  482. def test_bigint(self):
  483. context = quickjs.Context()
  484. self.assertEqual(context.eval(f"BigInt('{10**100}')"), 10**100)
  485. self.assertEqual(context.eval(f"BigInt('{-10**100}')"), -10**100)
  486. class Threads(unittest.TestCase):
  487. def setUp(self):
  488. self.context = quickjs.Context()
  489. self.executor = concurrent.futures.ThreadPoolExecutor()
  490. def tearDown(self):
  491. self.executor.shutdown()
  492. def test_concurrent(self):
  493. """Demonstrates that the execution will crash unless the function executes on the same
  494. thread every time.
  495. If the executor in Function is not present, this test will fail.
  496. """
  497. data = list(range(1000))
  498. jssum = quickjs.Function(
  499. "sum", """
  500. function sum(data) {
  501. return data.reduce((a, b) => a + b, 0)
  502. }
  503. """)
  504. futures = [self.executor.submit(jssum, data) for _ in range(10)]
  505. expected = sum(data)
  506. for future in concurrent.futures.as_completed(futures):
  507. self.assertEqual(future.result(), expected)
  508. def test_concurrent_own_executor(self):
  509. data = list(range(1000))
  510. jssum1 = quickjs.Function("sum",
  511. """
  512. function sum(data) {
  513. return data.reduce((a, b) => a + b, 0)
  514. }
  515. """,
  516. own_executor=True)
  517. jssum2 = quickjs.Function("sum",
  518. """
  519. function sum(data) {
  520. return data.reduce((a, b) => a + b, 0)
  521. }
  522. """,
  523. own_executor=True)
  524. futures = [self.executor.submit(f, data) for _ in range(10) for f in (jssum1, jssum2)]
  525. expected = sum(data)
  526. for future in concurrent.futures.as_completed(futures):
  527. self.assertEqual(future.result(), expected)
  528. class QJS(object):
  529. def __init__(self):
  530. self.interp = quickjs.Context()
  531. self.interp.eval('var foo = "bar";')
  532. class QuickJSContextInClass(unittest.TestCase):
  533. def test_github_issue_7(self):
  534. # This used to give stack overflow internal error, due to how QuickJS calculates stack
  535. # frames. Passes with the 2021-03-27 release.
  536. #
  537. # TODO: Use the new JS_UpdateStackTop function in order to better handle stacks.
  538. qjs = QJS()
  539. self.assertEqual(qjs.interp.eval('2+2'), 4)