Compare commits

...

210 Commits

Author SHA1 Message Date
Kai Tamkun
4b2351a12c Merge branch 'ben/fix-node-napi-tests' into kai/fix-node-napi-tests 2024-12-02 14:11:20 -08:00
Kai Tamkun
7ee91a9912 Merge branch 'main' into ben/fix-node-napi-tests 2024-12-02 12:44:04 -08:00
Kai Tamkun
769c6de751 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-26 12:23:24 -08:00
Kai Tamkun
c1a25d0948 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-25 19:18:45 -08:00
Kai Tamkun
1baa1b6975 Clean up missing globals code a bit 2024-11-25 13:18:42 -08:00
Kai Tamkun
1789364215 Add missing globals in napi tests 2024-11-25 12:59:52 -08:00
Kai Tamkun
bb33176924 Oops, again 2024-11-25 12:45:01 -08:00
Kai Tamkun
4a10bf22f7 Oops 2024-11-25 12:30:49 -08:00
Kai Tamkun
33d3732d44 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-25 11:56:32 -08:00
Kai Tamkun
78861829c9 Fix setRawMode return value on Windows 2024-11-22 17:54:37 -08:00
Kai Tamkun
e817928981 Reenable Worker test in js-native-api/test_instance_data/test.js 2024-11-22 13:22:17 -08:00
Kai Tamkun
3fc6ad4982 Increase napi timeout 2024-11-21 12:28:01 -08:00
Kai Tamkun
7a623fe3e8 Print compilation progress in node-napi.test.ts 2024-11-21 12:15:49 -08:00
Kai Tamkun
f78ac6344b Merge branch 'ben/fix-node-napi-tests' of github.com:oven-sh/bun into ben/fix-node-napi-tests 2024-11-21 12:13:18 -08:00
Ben Grant
94260398b0 Add Prisma leak test from #15289 2024-11-20 19:44:10 -08:00
Kai Tamkun
81690617c0 Revert "Maybe increase the timeout with setDefaultTimeout too"
This reverts commit 30eda1eca9.
2024-11-20 19:24:47 -08:00
Kai Tamkun
30eda1eca9 Maybe increase the timeout with setDefaultTimeout too 2024-11-20 19:12:52 -08:00
Kai Tamkun
e0414d0890 Allow ten minutes for compiling node-napi tests 2024-11-20 18:39:50 -08:00
Kai Tamkun
b191968681 Skip test_callback_scope/test.js on Windows 2024-11-20 18:24:24 -08:00
Kai Tamkun
2406936f33 Merge branch 'ben/fix-node-napi-tests' of github.com:oven-sh/bun into ben/fix-node-napi-tests 2024-11-20 16:54:54 -08:00
Kai Tamkun
24a3f96359 Flush stdout explicitly in test_cleanup_hook 2024-11-20 16:54:48 -08:00
Ben Grant
7a73f14da7 Fix napi_threadsafe_function memory leak with fixed queue size 2024-11-19 18:11:23 -08:00
Ben Grant
6ba0563d2d Add test for napi_threadsafe_function memory leak 2024-11-19 18:11:22 -08:00
Kai Tamkun
dec572eb4b Merge branch 'ben/fix-node-napi-tests' of github.com:oven-sh/bun into ben/fix-node-napi-tests 2024-11-19 15:55:57 -08:00
Kai Tamkun
01bbe3070a Disable part of js-native-api/test_instance_data/test.js 2024-11-19 15:55:05 -08:00
Ben Grant
b0a30ca422 Fix leak in napi_threadsafe_function 2024-11-19 13:30:21 -08:00
Kai Tamkun
1e649b4976 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-19 12:19:00 -08:00
Kai Tamkun
5949777ec3 Or maybe this might 2024-11-18 16:45:32 -08:00
Kai Tamkun
7f9935a560 Will this fix the Windows timeout problem? 2024-11-18 16:35:54 -08:00
Kai Tamkun
437d333978 Actually never mind on that last one 2024-11-18 16:18:45 -08:00
Kai Tamkun
c38eca222e Node NAPI tests: get clang(++) from $PATH in CI 2024-11-18 16:12:46 -08:00
Kai Tamkun
d93122a656 Typedef napi_env in napi_with_version.h and then use it in the node_api_basic_env typedef 2024-11-18 16:12:07 -08:00
Kai Tamkun
b02fb2463f Add Bun__crashHandler binding 2024-11-18 14:50:33 -08:00
Kai Tamkun
ba5490dafc Change abort exit code on Windows 2024-11-18 14:43:43 -08:00
Kai Tamkun
3e085b5540 Cast argument to quick_exit in crash() 2024-11-18 13:11:40 -08:00
Kai Tamkun
73e98663bb Merge branch 'main' into ben/fix-node-napi-tests 2024-11-18 13:00:54 -08:00
Kai Tamkun
d09050127f Change Windows crash behavior 2024-11-18 12:09:15 -08:00
Kai Tamkun
eef79ce772 Skip test_worker_buffer_callback/test-free-called.js 2024-11-15 20:08:26 -08:00
Kai Tamkun
cf960b5c17 Change how GC is detected 2024-11-15 14:46:24 -08:00
Ben Grant
6603871617 Add logs for NAPI refs and handle scopes 2024-11-15 12:56:52 -08:00
Ben Grant
e5c5033790 Use clang 16 to compile Node NAPI tests on CI linux 2024-11-15 12:56:52 -08:00
Kai Tamkun
07252d1755 Add missing include 2024-11-15 12:40:00 -08:00
Kai Tamkun
d9c8f27bf9 Merge branch 'ben/fix-node-napi-tests' into kai/fix-node-napi-tests 2024-11-14 21:36:05 -08:00
Kai Tamkun
28830f0294 Don't defer finalizers if not in GC 2024-11-14 21:35:23 -08:00
Kai Tamkun
2aee62382f Remove async cleanup hooks from the list after they're called, not before 2024-11-14 17:57:52 -08:00
Kai Tamkun
4103b738ff Report NAPI assertion failures more forcefully 2024-11-14 17:56:14 -08:00
Ben Grant
f73ef54edd Compile Node tests using Node 23 headers 2024-11-14 15:59:02 -08:00
Kai Tamkun
f9718af6a5 Make checkGC fail if running a finalizer during napi env cleanup 2024-11-14 15:17:28 -08:00
Kai Tamkun
f50114332f Fix incorrect calling convention usage 2024-11-14 15:09:26 -08:00
Ben Grant
90852a37d5 Use correct path separators on Windows 2024-11-14 14:41:20 -08:00
Ben Grant
2afb5e635d Skip compiling Node NAPI tests that we skip running 2024-11-14 14:24:52 -08:00
Kai Tamkun
134f66c24d Undo accidental formatting 2024-11-14 13:32:08 -08:00
Kai Tamkun
f37df906b4 Add typedef for node_api_basic_env in napi tests 2024-11-14 13:28:52 -08:00
Kai Tamkun
ed1f25e5cc Skip node-api/test_async_cleanup_hook/test.js because it uses libuv functions 2024-11-13 19:59:38 -08:00
Kai Tamkun
2646ea0956 Fix async cleanup hooks? 2024-11-13 19:53:10 -08:00
Kai Tamkun
9fa480ce9b Fix env cleanup hooks 2024-11-13 19:26:56 -08:00
Kai Tamkun
83a2c245f3 Merge branch 'ben/fix-node-napi-tests' into kai/fix-node-napi-tests 2024-11-13 18:36:45 -08:00
Ben Grant
e11a68315b Fix compile error 2024-11-13 17:30:25 -08:00
Ben Grant
f439dacf21 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-13 17:27:12 -08:00
Ben Grant
544dd2497c Increase timeout 2024-11-13 16:59:45 -08:00
Ben Grant
0e7ed996d3 Remove stray import 2024-11-13 16:17:53 -08:00
Ben Grant
bb8b46507e Remove workaround for #15111 2024-11-13 15:53:13 -08:00
Kai Tamkun
06d37bf644 Actually call instance data finalizers 2024-11-13 15:32:16 -08:00
Kai Tamkun
6dd369e66b Merge branch 'ben/fix-node-napi-tests' into kai/fix-node-napi-tests 2024-11-13 15:23:27 -08:00
Ben Grant
8358f4dc73 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-13 15:20:55 -08:00
Ben Grant
9dbe40ddba Compile tests in parallel 2024-11-13 15:20:43 -08:00
Kai Tamkun
f54f4e6ebf Remove stray iostream include 2024-11-13 15:19:54 -08:00
Kai Tamkun
adc00e0566 Don't defer finalizers if the VM is shutting down 2024-11-13 15:11:06 -08:00
Kai Tamkun
d3b509e80a Merge branch 'ben/fix-node-napi-tests' into kai/fix-node-napi-tests 2024-11-13 14:43:48 -08:00
Kai Tamkun
b11d631e41 Ensure NapiRef finalizers are called by the time the env is cleaned up 2024-11-13 14:42:28 -08:00
190n
440111f924 bun run prettier:extra 2024-11-13 22:01:31 +00:00
Ben Grant
ccd72755dc Remove console log 2024-11-13 14:00:07 -08:00
Ben Grant
59700068d3 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-13 13:55:03 -08:00
Ben Grant
ac8c6f093b Run some of Node's Node-API tests in CI 2024-11-13 13:54:45 -08:00
Kai Tamkun
a8fa566101 Fix beforeOnExit being dispatched multiple times 2024-11-12 22:43:05 -08:00
Kai Tamkun
7b25ce15eb Merge branch 'ben/fix-node-napi-tests' into kai/fix-node-napi-tests 2024-11-12 21:28:02 -08:00
Kai Tamkun
c20c0dea92 Undo the previous commit and add a TODO about Web Worker termination 2024-11-12 21:01:48 -08:00
Kai Tamkun
86d4dbe143 Treat a call to process.exit() inside napi_call_function as if it threw an exception 2024-11-12 20:29:04 -08:00
190n
83f536f4da Make parameter const 2024-11-12 17:57:14 -08:00
Kai Tamkun
d11a48398d Merge branch 'ben/fix-node-napi-tests' into kai/fix-node-napi-tests 2024-11-12 14:43:54 -08:00
Kai Tamkun
563b3c0339 Address more feedback 2024-11-12 14:38:35 -08:00
Kai Tamkun
6e7240b6e7 Derefcountify NapiFinalizer 2024-11-12 14:08:38 -08:00
Kai Tamkun
2335e35a86 Remove defer parameter from NapiRef constructor 2024-11-12 13:57:50 -08:00
Kai Tamkun
bd45a65f2b Fix napi module file URIs 2024-11-12 13:45:02 -08:00
Kai Tamkun
7993f4fa11 Address some feedback 2024-11-12 13:44:37 -08:00
Kai Tamkun
09a6a11a14 Don't needlessly tick the event loop if the napi finalizer queue is empty 2024-11-12 12:59:49 -08:00
Kai Tamkun
c17e05c191 Better use of types in FFI.h 2024-11-12 12:58:12 -08:00
Kai Tamkun
9ea9925e9c Rename napiEnv FFI symbol to avoid potential collisions 2024-11-12 12:06:45 -08:00
Ben Grant
469be87987 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-12 12:00:07 -08:00
Ben Grant
9490c30d47 Remove stray include 2024-11-12 11:53:18 -08:00
Kai Tamkun
1d8423ea57 Allow null data in napi_async_work 2024-11-12 11:46:52 -08:00
Kai Tamkun
0bee1c9b5d Don't create napi_envs for FFI unless actually needed 2024-11-12 11:45:40 -08:00
Ben Grant
3a71be377e Use node_api_post_finalize in napi-app 2024-11-11 17:24:34 -08:00
Ben Grant
1de2319526 Update TODO in napi-app 2024-11-11 17:23:02 -08:00
Ben Grant
8b5fb349dd Assert there is an env when calling external finalizer 2024-11-11 16:31:58 -08:00
Ben Grant
657f5b9f6a Set WebKit version to merge commit instead of commit on branch of oven-sh/WebKit#68 2024-11-11 13:40:47 -08:00
Ben Grant
86e421ad80 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-11 13:40:14 -08:00
Kai Tamkun
e34673ca45 Fix test_reference_by_node_api_version 2024-11-08 18:07:06 -08:00
Kai Tamkun
7c13e637b8 Update WebKit version 2024-11-08 17:46:44 -08:00
Kai Tamkun
9f3b0f754b Adjust some NAPI error messages to match Node equivalents 2024-11-08 15:07:50 -08:00
Kai Tamkun
07a391368f Escape NAPI file:// URIs 2024-11-08 15:07:13 -08:00
Kai Tamkun
c30ef2ccc8 Fix more missing includes in napi-app 2024-11-07 17:03:20 -08:00
Kai Tamkun
855b7101e6 Use node_api_post_finalizer in napi-app and add some missing includes 2024-11-07 16:39:13 -08:00
Kai Tamkun
fceeb228a8 Don't dlclose if node_api_module_get_api_version_v1 is missing 2024-11-07 16:17:10 -08:00
Kai Tamkun
85dcebedd7 Fix typedef in napi-app 2024-11-07 15:40:56 -08:00
Kai Tamkun
0f29267a3e Add node_api_get_module_file_name 2024-11-07 15:16:22 -08:00
Kai Tamkun
a152557096 Fix napi_env usage in FFI 2024-11-07 15:16:06 -08:00
Kai Tamkun
80b742665e Add node_api_create_buffer_from_arraybuffer 2024-11-06 14:21:58 -08:00
Kai Tamkun
7978505b94 Fix napi_get_buffer_info return code 2024-11-06 14:21:39 -08:00
Kai Tamkun
0dce7366e2 oops 2024-11-05 17:18:56 -08:00
Kai Tamkun
71f3089f4d Defer external (array)buffer finalizers if necessary 2024-11-05 17:18:11 -08:00
Kai Tamkun
07a217f773 Fix napi_get_version misunderstanding 2024-11-05 16:43:12 -08:00
Kai Tamkun
d4b710287f Clear NAPI error after native function invocations 2024-11-05 15:21:15 -08:00
Kai Tamkun
3296a6edc9 Fix napi_instanceof 2024-11-05 13:32:07 -08:00
Kai Tamkun
ab92fc5fab Add a typedef for node_api_basic_env in napi-app 2024-11-05 11:43:01 -08:00
Kai Tamkun
f5dc0498f4 Fix some formatting 2024-11-05 11:41:17 -08:00
Kai Tamkun
1c06dbd3ef Fix multiple dispatch of beforeExit 2024-11-05 10:58:22 -08:00
Kai Tamkun
a6d707a74e Don't require node_api_module_get_api_version_v1, default to version 8 2024-11-04 20:12:44 -08:00
Kai Tamkun
ce469474d8 Use WebKit branch kai/inherited-property-names 2024-11-04 19:54:07 -08:00
Kai Tamkun
c659b3b7d3 Require key to be a string or symbol in napi_has_own_property 2024-11-04 15:59:24 -08:00
Kai Tamkun
a60ae54751 Merge branch 'ben/fix-node-napi-tests' of github.com:oven-sh/bun into ben/fix-node-napi-tests 2024-11-04 15:47:32 -08:00
Kai Tamkun
3303a5de1f Improve finalizer support 2024-11-04 15:47:23 -08:00
Ben Grant
d8557ea982 Merge branch 'main' into ben/fix-node-napi-tests 2024-11-04 09:49:35 -08:00
Ben Grant
e3b5927e73 Work on NAPI leak tests 2024-11-01 19:09:49 -07:00
Ben Grant
066b1dacc4 Fix errors from rebase 2024-11-01 18:36:46 -07:00
Ben Grant
7a20f515f7 Fix NAPI property tests 2024-11-01 18:30:47 -07:00
Ben Grant
afd023ac95 Fix remaining error in NAPI property functions 2024-11-01 18:30:47 -07:00
Jarred Sumner
1d5da9ef77 Fixes #11754 (#14948) 2024-11-01 18:30:47 -07:00
Jarred Sumner
7110c073ca Inline process.versions.bun in bun build --compile (#14940) 2024-11-01 18:30:47 -07:00
Ashcon Partovi
03d945ee05 Run tests from npm packages, elysia to start (#14932) 2024-11-01 18:30:47 -07:00
190n
08116e43f4 Fix napi property methods on non-objects (#14935) 2024-11-01 18:30:45 -07:00
Dylan Conway
7fab6701e5 Redact secrets in bunfig.toml and npmrc logs (#14919) 2024-11-01 18:23:06 -07:00
Dylan Conway
30fe8d5258 fix(install): only globally link requested packages (#12506)
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2024-11-01 18:23:06 -07:00
Jarred Sumner
a8a2403568 Add bytesWritten property to Bun.Socket, fix encoding issue in node:net (#14516)
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
2024-11-01 18:23:06 -07:00
Jarred Sumner
664c080d02 Fixes #14918 (#14921) 2024-11-01 18:23:06 -07:00
Ashcon Partovi
b5ed0f028f ci: If only tests change, use artifacts from last successful build (#14927) 2024-11-01 18:23:06 -07:00
Jarred Sumner
1293039002 Clean up some code in node validators (#14897) 2024-11-01 18:23:06 -07:00
Kai Tamkun
8bb8193a39 Fix use after free 2024-11-01 16:55:07 -07:00
Kai Tamkun
dffc718b6a Merge branch 'ben/fix-node-napi-tests' of github.com:oven-sh/bun into ben/fix-node-napi-tests 2024-11-01 16:36:15 -07:00
Kai Tamkun
f15059face Handle exceptions in NAPI finalizers as uncaught exceptions 2024-11-01 16:36:01 -07:00
Ben Grant
2d96ec0e21 Fix napi_set_property 2024-11-01 15:47:31 -07:00
Ben Grant
b6dfd89928 Remove reference to nonexistent test 2024-11-01 15:42:46 -07:00
Ben Grant
936ae5a796 Fix and test edge cases calling NAPI constructors 2024-11-01 15:32:18 -07:00
Ben Grant
fe18b871f8 Move napi_wrap lifetime test to existing test suite 2024-11-01 15:32:16 -07:00
Kai Tamkun
a1c4240940 Integrate #12660 into bun/fix-node-napi-tests 2024-11-01 11:58:48 -07:00
Kai Tamkun
dc4177f113 Keep napi_envs separate, keep track of them, associate module info
This is so we can check the NAPI version per module and adjust behavior accordingly.
2024-10-31 20:54:10 -07:00
Kai Tamkun
d2c4a9a57e Fix napi_get_all_property_names not including inherited properties when desired 2024-10-31 16:45:17 -07:00
Kai Tamkun
fb6a48a35f Fix property-related NAPI methods 2024-10-31 15:16:42 -07:00
Ben Grant
5176ab58bc Test napi_set_property and napi_set_named_property 2024-10-31 13:21:26 -07:00
Ben Grant
a669ff1243 Build napi-app in debug mode 2024-10-31 12:19:55 -07:00
Ben Grant
91a52311de Merge branch 'main' into ben/fix-node-napi-tests 2024-10-31 12:16:56 -07:00
Kai Tamkun
197a26fc16 Fix some confusion over the "length" parameter in NapiClass 2024-10-31 11:21:31 -07:00
Kai Tamkun
059185f4ba Fix napi_get_new_target 2024-10-31 10:58:49 -07:00
Kai Tamkun
838ca008cd Replace NAPIFunction with NapiClass 2024-10-30 18:55:27 -07:00
Kai Tamkun
8a0a88cd42 Merge branch 'ben/fix-node-napi-tests' of github.com:oven-sh/bun into ben/fix-node-napi-tests 2024-10-30 12:59:05 -07:00
Kai Tamkun
774bb8923d Fix node_api_symbol_for behavior for null description parameters 2024-10-30 12:55:47 -07:00
Ben Grant
8b19e08882 Rename wrap-lifetime-test.mjs 2024-10-30 12:24:32 -07:00
Ben Grant
84c4f96b3f Merge branch 'main' into ben/fix-node-napi-tests 2024-10-30 10:01:32 -07:00
Ben Grant
73579e1254 Support filtering by property descriptor in napi_get_all_property_names 2024-10-29 18:18:04 -07:00
Kai Tamkun
19b0fed84f Include all properties except enums in napi_get_property_names, and ensure that the key is a name in napi_has_own_property 2024-10-29 15:24:58 -07:00
Kai Tamkun
699997826f Make napi_create_external_arraybuffer produce a non-shared ArrayBuffer 2024-10-29 14:00:41 -07:00
Kai Tamkun
528d9a64e8 Merge branch 'main' into ben/fix-node-napi-tests 2024-10-29 13:37:59 -07:00
Kai Tamkun
39b442b664 Move napi_create_typedarray from napi.zig to napi.cpp to produce RangeError exceptions 2024-10-29 13:37:29 -07:00
Kai Tamkun
249227d4d6 Fix NAPI string creation bugs 2024-10-28 19:26:19 -07:00
Ben Grant
71101e1fa3 Fix NAPI bugs 2024-10-28 17:43:11 -07:00
Ben Grant
c28d419b25 Reset last NAPI error before calling into a native module 2024-10-28 14:03:59 -07:00
Ben Grant
3587391920 Add napi_type_tag_object and napi_check_type_tag 2024-10-25 18:43:19 -07:00
Ben Grant
43d7cfcb23 Fix NAPI tests compiling on Windows 2024-10-25 18:42:53 -07:00
Ben Grant
3ba398f482 Stress test napi_wrap and napi_external 2024-10-25 16:56:54 -07:00
Ben Grant
600bc1cbd2 Fix napi_ref finalizers 2024-10-25 16:56:33 -07:00
Ben Grant
f71b440c4d Add filename to napi_log 2024-10-25 16:54:55 -07:00
Ben Grant
d29e72f89c Add missing #include 2024-10-25 10:08:44 -07:00
Ben Grant
e04f461508 Work on leak testing for NAPI wrap/ref/external 2024-10-24 18:33:55 -07:00
Ben Grant
bdcca417ef Refine NAPI tests 2024-10-24 18:05:51 -07:00
Ben Grant
b8aba83da6 napi_wrap fixes 2024-10-24 18:02:28 -07:00
Ben Grant
b753e4b38b Fix providing class's data pointer to method without data pointer 2024-10-23 18:46:20 -07:00
Ben Grant
a7bc53b92c Split NAPI tests out of the huge C++ file 2024-10-23 18:29:48 -07:00
Ben Grant
b2080c88f4 JS exceptions instead of assertions in napi tests 2024-10-23 15:02:26 -07:00
Ben Grant
710f7790cf Delete NapiPrototype::napiRef 2024-10-23 15:01:58 -07:00
Ben Grant
c44eb732ee Do not propagate nullptr out of NAPIFunction::call 2024-10-23 11:24:52 -07:00
Ben Grant
e5e643d8bc Merge branch 'main' into ben/fix-node-napi-tests 2024-10-23 10:19:02 -07:00
Ben Grant
d612cfff12 Misc CallFrame fixes 2024-10-22 19:07:29 -07:00
Ben Grant
020c32bc73 clangd config for NAPI tests 2024-10-22 18:59:35 -07:00
Ben Grant
a240093a97 Pass data pointer to NAPI constructors 2024-10-21 11:21:48 -07:00
Ben Grant
e5ffd66649 Merge branch 'main' into ben/fix-node-napi-tests 2024-10-21 10:18:05 -07:00
Ben Grant
5bae294c14 More (self-contained for now) napi tests 2024-10-18 18:43:22 -07:00
Ben Grant
ea1ddb2740 napi_wrap WIP + rip out NAPICallFrame tagging 2024-10-18 18:42:51 -07:00
Ben Grant
6d1db2c8e9 WIP supporting napi_wrap for more values 2024-10-17 17:56:06 -07:00
Ben Grant
8c571d8949 Merge branch 'main' into ben/fix-node-napi-tests 2024-10-16 18:49:21 -07:00
Ben Grant
7be1bf3026 Fix napi_create_dataview 2024-10-16 17:26:33 -07:00
Ben Grant
2d0e0c9195 Set error code in Zig napi functions 2024-10-16 17:02:57 -07:00
Ben Grant
b773e66d67 Move napi_get_value_string_{latin1,utf16} to C++ 2024-10-16 17:02:25 -07:00
Ben Grant
2fee09fc4d Change sizeof division to std::size 2024-10-16 14:00:57 -07:00
Ben Grant
216e5b3f96 Fix napi_coerce_to_* 2024-10-15 18:40:54 -07:00
Ben Grant
ed4175b80e Arg checking in napi_define_class 2024-10-15 16:17:45 -07:00
Ben Grant
a0c2a73730 Fix napi_create_bigint_words error handling 2024-10-15 16:10:32 -07:00
Ben Grant
1649c03824 Merge branch 'main' into ben/fix-node-napi-tests 2024-10-15 14:59:57 -07:00
Ben Grant
49b2de93d0 Set last error in all C++ napi functions 2024-10-15 13:59:58 -07:00
Ben Grant
ef4728c267 WIP convert napi.cpp functions to set the last error 2024-10-14 18:40:20 -07:00
Ben Grant
6a440aa946 Merge branch 'main' into ben/fix-node-napi-tests 2024-10-14 11:28:30 -07:00
Ben Grant
6169f1053a Fix bugs with napi_define_class 2024-10-11 19:10:49 -07:00
Ben Grant
85f617f97e Fix bugs in napi_create_class and napi_get_value_bigint_* 2024-10-11 18:45:58 -07:00
Ben Grant
23dc0fed71 Test that threadsafe function finalizers run on the next tick 2024-10-04 15:24:01 -07:00
Ben Grant
afcf7b1eb6 Route all finalizers through NapiFinalizer 2024-10-04 15:23:23 -07:00
Ben Grant
2583f33a33 Make NAPI tests allocate in the finalizer 2024-10-04 11:08:37 -07:00
Ben Grant
dfa2a6b60b Fix tsfn finalizers 2024-10-03 18:48:25 -07:00
Ben Grant
e66ec2a10b Merge branch 'main' into jarred/napi-2 2024-10-03 17:56:38 -07:00
Dylan Conway
eb8d465c50 Merge branch 'main' into jarred/napi-2 2024-07-22 21:14:24 -07:00
Jarred-Sumner
418139358b Apply formatting changes 2024-07-19 08:20:27 +00:00
Jarred Sumner
1f5359705e Defer finalization for napi callbacks to the immediate task queue 2024-07-19 01:09:31 -07:00
335 changed files with 26743 additions and 3112 deletions

View File

@@ -45,6 +45,7 @@ const testsPath = join(cwd, "test");
const spawnTimeout = 5_000;
const testTimeout = 3 * 60_000;
const integrationTimeout = 5 * 60_000;
const napiTimeout = 10 * 60_000;
const { values: options, positionals: filters } = parseArgs({
allowPositionals: true,
@@ -580,6 +581,9 @@ function getTestTimeout(testPath) {
if (/integration|3rd_party|docker/i.test(testPath)) {
return integrationTimeout;
}
if (/napi/i.test(testPath)) {
return napiTimeout;
}
return testTimeout;
}

View File

@@ -73,6 +73,8 @@ const URL = @import("../../url.zig").URL;
const VirtualMachine = JSC.VirtualMachine;
const IOTask = JSC.IOTask;
const napi = @import("../../napi/napi.zig");
const TCC = @import("../../tcc.zig");
extern fn pthread_jit_write_protect_np(enable: bool) callconv(.C) void;
@@ -446,7 +448,7 @@ pub const FFI = struct {
for (this.symbols.map.values()) |*symbol| {
if (symbol.needsNapiEnv()) {
_ = TCC.tcc_add_symbol(state, "Bun__thisFFIModuleNapiEnv", globalThis);
_ = TCC.tcc_add_symbol(state, "Bun__thisFFIModuleNapiEnv", globalThis.makeNapiEnvForFFI());
break;
}
}
@@ -800,12 +802,14 @@ pub const FFI = struct {
// we are unable to free memory safely in certain cases here.
}
const napi_env = makeNapiEnvIfNeeded(compile_c.symbols.map.values(), globalThis);
var obj = JSC.JSValue.createEmptyObject(globalThis, compile_c.symbols.map.count());
for (compile_c.symbols.map.values()) |*function| {
const function_name = function.base_name.?;
const allocator = bun.default_allocator;
function.compile(allocator, globalThis) catch |err| {
function.compile(allocator, napi_env) catch |err| {
if (!globalThis.hasException()) {
const ret = JSC.toInvalidArguments("{s} when translating symbol \"{s}\"", .{
@errorName(err),
@@ -1126,6 +1130,9 @@ pub const FFI = struct {
var obj = JSC.JSValue.createEmptyObject(global, size);
obj.protect();
defer obj.unprotect();
const napi_env = makeNapiEnvIfNeeded(symbols.values(), global);
for (symbols.values()) |*function| {
const function_name = function.base_name.?;
@@ -1145,7 +1152,7 @@ pub const FFI = struct {
function.symbol_from_dynamic_library = resolved_symbol;
}
function.compile(allocator, global) catch |err| {
function.compile(allocator, napi_env) catch |err| {
const ret = JSC.toInvalidArguments("{s} when compiling symbol \"{s}\" in \"{s}\"", .{
bun.asByteSlice(@errorName(err)),
bun.asByteSlice(function_name),
@@ -1237,6 +1244,9 @@ pub const FFI = struct {
var obj = JSValue.createEmptyObject(global, symbols.count());
obj.ensureStillAlive();
defer obj.ensureStillAlive();
const napi_env = makeNapiEnvIfNeeded(symbols.values(), global);
for (symbols.values()) |*function| {
const function_name = function.base_name.?;
@@ -1250,7 +1260,7 @@ pub const FFI = struct {
return ret;
}
function.compile(allocator, global) catch |err| {
function.compile(allocator, napi_env) catch |err| {
const ret = JSC.toInvalidArguments("{s} when compiling symbol \"{s}\"", .{
bun.asByteSlice(@errorName(err)),
bun.asByteSlice(function_name),
@@ -1556,11 +1566,7 @@ pub const FFI = struct {
const tcc_options = "-std=c11 -nostdlib -Wl,--export-all-symbols" ++ if (Environment.isDebug) " -g" else "";
pub fn compile(
this: *Function,
allocator: std.mem.Allocator,
globalObject: *JSC.JSGlobalObject,
) !void {
pub fn compile(this: *Function, allocator: std.mem.Allocator, napiEnv: ?*napi.NapiEnv) !void {
var source_code = std.ArrayList(u8).init(allocator);
var source_code_writer = source_code.writer();
try this.printSourceCode(&source_code_writer);
@@ -1582,7 +1588,9 @@ pub const FFI = struct {
_ = TCC.tcc_set_output_type(state, TCC.TCC_OUTPUT_MEMORY);
_ = TCC.tcc_add_symbol(state, "Bun__thisFFIModuleNapiEnv", globalObject);
if (napiEnv) |env| {
_ = TCC.tcc_add_symbol(state, "Bun__thisFFIModuleNapiEnv", env);
}
CompilerRT.define(state);
@@ -1691,7 +1699,9 @@ pub const FFI = struct {
_ = TCC.tcc_set_output_type(state, TCC.TCC_OUTPUT_MEMORY);
_ = TCC.tcc_add_symbol(state, "Bun__thisFFIModuleNapiEnv", js_context);
if (this.needsNapiEnv()) {
_ = TCC.tcc_add_symbol(state, "Bun__thisFFIModuleNapiEnv", js_context.makeNapiEnvForFFI());
}
CompilerRT.define(state);
@@ -2550,3 +2560,13 @@ const CompilerRT = struct {
};
pub const Bun__FFI__cc = FFI.Bun__FFI__cc;
fn makeNapiEnvIfNeeded(functions: []const FFI.Function, globalThis: *JSGlobalObject) ?*napi.NapiEnv {
for (functions) |function| {
if (function.needsNapiEnv()) {
return globalThis.makeNapiEnvForFFI();
}
}
return null;
}

View File

@@ -1,3 +1,5 @@
#include "napi.h"
#include "BunProcess.h"
#include <JavaScriptCore/InternalFieldTuple.h>
#include <JavaScriptCore/JSMicrotask.h>
@@ -13,7 +15,6 @@
#include "JavaScriptCore/PutPropertySlot.h"
#include "ScriptExecutionContext.h"
#include "headers-handwritten.h"
#include "node_api.h"
#include "ZigGlobalObject.h"
#include "headers.h"
#include "JSEnvironmentVariableMap.h"
@@ -31,6 +32,8 @@
#include <JavaScriptCore/LazyPropertyInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
#include "wtf-bindings.h"
#include "EventLoopTask.h"
#include <webcore/SerializedScriptValue.h>
#include "ProcessBindingTTYWrap.h"
#include "wtf/text/ASCIILiteral.h"
@@ -87,6 +90,8 @@ typedef int mode_t;
#include <unistd.h> // setuid, getuid
#endif
#include <cstring>
namespace Bun {
using namespace JSC;
@@ -278,6 +283,69 @@ extern "C" bool Bun__resolveEmbeddedNodeFile(void*, BunString*);
extern "C" HMODULE Bun__LoadLibraryBunString(BunString*);
#endif
/// Returns a pointer that needs to be freed with `delete[]`.
static char* toFileURI(std::string_view path)
{
auto needs_escape = [](char ch) {
return !(('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9')
|| ch == '_' || ch == '-' || ch == '.' || ch == '!' || ch == '~' || ch == '*' || ch == '\'' || ch == '(' || ch == ')' || ch == '/' || ch == ':');
};
auto to_hex = [](uint8_t nybble) -> char {
if (nybble < 0xa) {
return '0' + nybble;
}
return 'a' + (nybble - 0xa);
};
size_t escape_count = 0;
for (char ch : path) {
#if OS(WINDOWS)
if (needs_escape(ch) && ch != '\\') {
#else
if (needs_escape(ch)) {
#endif
++escape_count;
}
}
#if OS(WINDOWS)
#define FILE_URI_START "file:///"
#else
#define FILE_URI_START "file://"
#endif
const size_t string_size = sizeof(FILE_URI_START) + path.size() + 2 * escape_count; // null byte is included in the sizeof expression
char* characters = new char[string_size];
strncpy(characters, FILE_URI_START, sizeof(FILE_URI_START));
size_t i = sizeof(FILE_URI_START) - 1;
for (char ch : path) {
#if OS(WINDOWS)
if (ch == '\\') {
characters[i++] = '/';
continue;
}
#endif
if (needs_escape(ch)) {
characters[i++] = '%';
characters[i++] = to_hex(static_cast<uint8_t>(ch) >> 4);
characters[i++] = to_hex(ch & 0xf);
} else {
characters[i++] = ch;
}
}
characters[i] = '\0';
ASSERT(i + 1 == string_size);
return characters;
}
static char* toFileURI(std::span<const uint8_t> span)
{
return toFileURI(std::string_view(reinterpret_cast<const char*>(span.data()), span.size()));
}
extern "C" size_t Bun__process_dlopen_count;
JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen,
@@ -394,18 +462,17 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen,
return JSValue::encode(jsUndefined());
}
JSC::EncodedJSValue (*napi_register_module_v1)(JSC::JSGlobalObject* globalObject,
JSC::EncodedJSValue exports);
#if OS(WINDOWS)
#define dlsym GetProcAddress
#endif
// TODO(@190n) look for node_register_module_vXYZ according to BuildOptions.reported_nodejs_version
// (bun/src/env.zig:36) and the table at https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json
napi_register_module_v1 = reinterpret_cast<JSC::EncodedJSValue (*)(JSC::JSGlobalObject*,
JSC::EncodedJSValue)>(
auto napi_register_module_v1 = reinterpret_cast<napi_value (*)(napi_env, napi_value)>(
dlsym(handle, "napi_register_module_v1"));
auto node_api_module_get_api_version_v1 = reinterpret_cast<int32_t (*)()>(dlsym(handle, "node_api_module_get_api_version_v1"));
#if OS(WINDOWS)
#undef dlsym
#endif
@@ -416,14 +483,39 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen,
#else
dlclose(handle);
#endif
JSC::throwTypeError(globalObject, scope, "symbol 'napi_register_module_v1' not found in native module. Is this a Node API (napi) module?"_s);
return {};
}
// TODO(@heimskr): get the API version without node_api_module_get_api_version_v1 a different way
int module_version = 8;
if (node_api_module_get_api_version_v1) {
module_version = node_api_module_get_api_version_v1();
}
NapiHandleScope handleScope(globalObject);
EncodedJSValue exportsValue = JSC::JSValue::encode(exports);
JSC::JSValue resultValue = JSValue::decode(napi_register_module_v1(globalObject, exportsValue));
char* filename_cstr = toFileURI(filename.utf8().span());
napi_module nmodule {
.nm_version = module_version,
.nm_flags = 0,
.nm_filename = filename_cstr,
.nm_register_func = nullptr,
.nm_modname = "[no modname]",
.nm_priv = nullptr,
.reserved = {},
};
static_assert(sizeof(napi_value) == sizeof(EncodedJSValue), "EncodedJSValue must be reinterpretable as a pointer");
auto env = globalObject->makeNapiEnv(nmodule);
env->filename = filename_cstr;
JSC::JSValue resultValue = JSValue::decode(reinterpret_cast<EncodedJSValue>(napi_register_module_v1(env, reinterpret_cast<napi_value>(exportsValue))));
RETURN_IF_EXCEPTION(scope, {});
@@ -2707,6 +2799,18 @@ extern "C" void Bun__Process__queueNextTick1(GlobalObject* globalObject, Encoded
}
JSC_DECLARE_HOST_FUNCTION(Bun__Process__queueNextTick1);
extern "C" bool Bun__queueFinishNapiFinalizers(JSGlobalObject* jsGlobalObject)
{
Zig::GlobalObject* globalObject = jsCast<Zig::GlobalObject*>(jsGlobalObject);
if (globalObject->hasNapiFinalizers()) {
globalObject->queueTask(new EventLoopTask([](ScriptExecutionContext&) {
// TODO(@heimskr): do something more sensible
}));
return true;
}
return false;
}
JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObject)
{
JSValue nextTickQueueObject;

View File

@@ -1176,13 +1176,11 @@ GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, const JSC::Gl
, m_worldIsNormal(true)
, m_builtinInternalFunctions(vm)
, m_scriptExecutionContext(new WebCore::ScriptExecutionContext(&vm, this))
, globalEventScope(*new Bun::WorkerGlobalScope(m_scriptExecutionContext))
, globalEventScope(adoptRef(*new Bun::WorkerGlobalScope(m_scriptExecutionContext)))
{
// m_scriptExecutionContext = globalEventScope.m_context;
mockModule = Bun::JSMockModule::create(this);
globalEventScope.m_context = m_scriptExecutionContext;
// FIXME: is there a better way to do this? this event handler should always be tied to the global object
globalEventScope.relaxAdoptionRequirement();
globalEventScope->m_context = m_scriptExecutionContext;
}
GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, WebCore::ScriptExecutionContextIdentifier contextId, const JSC::GlobalObjectMethodTable* methodTable)
@@ -1193,22 +1191,15 @@ GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, WebCore::Scri
, m_worldIsNormal(true)
, m_builtinInternalFunctions(vm)
, m_scriptExecutionContext(new WebCore::ScriptExecutionContext(&vm, this, contextId))
, globalEventScope(*new Bun::WorkerGlobalScope(m_scriptExecutionContext))
, globalEventScope(adoptRef(*new Bun::WorkerGlobalScope(m_scriptExecutionContext)))
{
// m_scriptExecutionContext = globalEventScope.m_context;
mockModule = Bun::JSMockModule::create(this);
globalEventScope.m_context = m_scriptExecutionContext;
// FIXME: is there a better way to do this? this event handler should always be tied to the global object
globalEventScope.relaxAdoptionRequirement();
globalEventScope->m_context = m_scriptExecutionContext;
}
GlobalObject::~GlobalObject()
{
if (napiInstanceDataFinalizer) {
napi_finalize finalizer = reinterpret_cast<napi_finalize>(napiInstanceDataFinalizer);
finalizer(toNapi(this), napiInstanceData, napiInstanceDataFinalizerHint);
}
if (auto* ctx = scriptExecutionContext()) {
ctx->removeFromContextsMap();
}
@@ -1916,7 +1907,7 @@ static inline JSC::EncodedJSValue jsFunctionAddEventListenerBody(JSC::JSGlobalOb
EnsureStillAliveScope argument2 = callFrame->argument(2);
auto options = argument2.value().isUndefined() ? false : convert<IDLUnion<IDLDictionary<AddEventListenerOptions>, IDLBoolean>>(*lexicalGlobalObject, argument2.value());
RETURN_IF_EXCEPTION(throwScope, {});
auto result = JSValue::encode(WebCore::toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.addEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); }));
auto result = JSValue::encode(WebCore::toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl->addEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); }));
RETURN_IF_EXCEPTION(throwScope, {});
vm.writeBarrier(&static_cast<JSObject&>(*castedThis), argument1.value());
return result;
@@ -1945,7 +1936,7 @@ static inline JSC::EncodedJSValue jsFunctionRemoveEventListenerBody(JSC::JSGloba
EnsureStillAliveScope argument2 = callFrame->argument(2);
auto options = argument2.value().isUndefined() ? false : convert<IDLUnion<IDLDictionary<EventListenerOptions>, IDLBoolean>>(*lexicalGlobalObject, argument2.value());
RETURN_IF_EXCEPTION(throwScope, {});
auto result = JSValue::encode(WebCore::toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.removeEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); }));
auto result = JSValue::encode(WebCore::toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl->removeEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); }));
RETURN_IF_EXCEPTION(throwScope, {});
vm.writeBarrier(&static_cast<JSObject&>(*castedThis), argument1.value());
return result;
@@ -1968,7 +1959,7 @@ static inline JSC::EncodedJSValue jsFunctionDispatchEventBody(JSC::JSGlobalObjec
EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0);
auto event = convert<IDLInterface<Event>>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "event"_s, "EventTarget"_s, "dispatchEvent"_s, "Event"_s); });
RETURN_IF_EXCEPTION(throwScope, {});
RELEASE_AND_RETURN(throwScope, JSValue::encode(WebCore::toJS<IDLBoolean>(*lexicalGlobalObject, throwScope, impl.dispatchEventForBindings(*event))));
RELEASE_AND_RETURN(throwScope, JSValue::encode(WebCore::toJS<IDLBoolean>(*lexicalGlobalObject, throwScope, impl->dispatchEventForBindings(*event))));
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionDispatchEvent, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
@@ -2466,7 +2457,6 @@ extern "C" JSC__JSValue ZigGlobalObject__readableStreamToJSON(Zig::GlobalObject*
return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments));
}
extern "C" JSC__JSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC__JSValue readableStreamValue);
extern "C" JSC__JSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC__JSValue readableStreamValue)
{
auto& vm = globalObject->vm();
@@ -2487,6 +2477,11 @@ extern "C" JSC__JSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject*
return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments));
}
extern "C" napi_env ZigGlobalObject__makeNapiEnvForFFI(Zig::GlobalObject* globalObject)
{
return globalObject->makeNapiEnvForFFI();
}
JSC_DECLARE_HOST_FUNCTION(functionReadableStreamToArrayBuffer);
JSC_DEFINE_HOST_FUNCTION(functionReadableStreamToArrayBuffer, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
@@ -3052,12 +3047,6 @@ void GlobalObject::finishCreation(VM& vm)
Bun::NapiExternal::createStructure(init.vm, init.owner, init.owner->objectPrototype()));
});
m_NAPIFunctionStructure.initLater(
[](const JSC::LazyProperty<JSC::JSGlobalObject, Structure>::Initializer& init) {
init.set(
Zig::createNAPIFunctionStructure(init.vm, init.owner));
});
m_NapiPrototypeStructure.initLater(
[](const JSC::LazyProperty<JSC::JSGlobalObject, Structure>::Initializer& init) {
init.set(
@@ -3072,6 +3061,10 @@ void GlobalObject::finishCreation(VM& vm)
init.set(Bun::NapiTypeTag::createStructure(init.vm, init.owner));
});
m_napiWraps.initLater([](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::JSWeakMap>::Initializer& init) {
init.set(JSC::JSWeakMap::create(init.vm, init.owner->weakMapStructure()));
});
m_napiTypeTags.initLater([](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::JSWeakMap>::Initializer& init) {
init.set(JSC::JSWeakMap::create(init.vm, init.owner->weakMapStructure()));
});
@@ -3821,7 +3814,6 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
thisObject->m_memoryFootprintStructure.visit(visitor);
thisObject->m_NapiClassStructure.visit(visitor);
thisObject->m_NapiExternalStructure.visit(visitor);
thisObject->m_NAPIFunctionStructure.visit(visitor);
thisObject->m_NapiPrototypeStructure.visit(visitor);
thisObject->m_NapiHandleScopeImplStructure.visit(visitor);
thisObject->m_NapiTypeTagStructure.visit(visitor);
@@ -3845,6 +3837,8 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
thisObject->m_utilInspectStylizeColorFunction.visit(visitor);
thisObject->m_utilInspectStylizeNoColorFunction.visit(visitor);
thisObject->m_vmModuleContextMap.visit(visitor);
thisObject->m_napiWraps.visit(visitor);
thisObject->m_napiTypeTags.visit(visitor);
thisObject->mockModule.activeSpySetStructure.visit(visitor);
thisObject->mockModule.mockFunctionStructure.visit(visitor);
thisObject->mockModule.mockImplementationStructure.visit(visitor);
@@ -3945,7 +3939,7 @@ void GlobalObject::visitAdditionalChildren(Visitor& visitor)
GlobalObject* thisObject = this;
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
thisObject->globalEventScope.visitJSEventListeners(visitor);
thisObject->globalEventScope->visitJSEventListeners(visitor);
ScriptExecutionContext* context = thisObject->scriptExecutionContext();
visitor.addOpaqueRoot(context);
@@ -4354,6 +4348,43 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h
}
}
napi_env GlobalObject::makeNapiEnv(const napi_module& mod)
{
m_napiEnvs.append(std::make_unique<napi_env__>(this, mod));
return m_napiEnvs.last().get();
}
napi_env GlobalObject::makeNapiEnvForFFI()
{
auto out = makeNapiEnv(napi_module {
.nm_version = 9,
.nm_flags = 0,
.nm_filename = "ffi://",
.nm_register_func = nullptr,
.nm_modname = "[ffi]",
.nm_priv = nullptr,
.reserved = {},
});
return out;
}
extern "C" bool Bun__isNapiFinalizerQueueEmpty(const JSGlobalObject*);
bool GlobalObject::hasNapiFinalizers() const
{
if (!Bun__isNapiFinalizerQueueEmpty(this)) {
return true;
}
for (const auto& env : m_napiEnvs) {
if (env->hasFinalizers()) {
return true;
}
}
return false;
}
#include "ZigGeneratedClasses+lazyStructureImpl.h"
#include "ZigGlobalObject.lut.h"

View File

@@ -53,6 +53,8 @@ class GlobalInternals;
#include "BunCommonStrings.h"
#include "BunHttp2CommonStrings.h"
#include "BunGlobalScope.h"
#include <js_native_api.h>
#include <node_api.h>
namespace WebCore {
class WorkerGlobalScope;
@@ -260,7 +262,6 @@ public:
Structure* NapiExternalStructure() const { return m_NapiExternalStructure.getInitializedOnMainThread(this); }
Structure* NapiPrototypeStructure() const { return m_NapiPrototypeStructure.getInitializedOnMainThread(this); }
Structure* NAPIFunctionStructure() const { return m_NAPIFunctionStructure.getInitializedOnMainThread(this); }
Structure* NapiHandleScopeImplStructure() const { return m_NapiHandleScopeImplStructure.getInitializedOnMainThread(this); }
Structure* NapiTypeTagStructure() const { return m_NapiTypeTagStructure.getInitializedOnMainThread(this); }
@@ -300,7 +301,7 @@ public:
WebCore::EventTarget& eventTarget();
WebCore::ScriptExecutionContext* m_scriptExecutionContext;
Bun::WorkerGlobalScope& globalEventScope;
Ref<Bun::WorkerGlobalScope> globalEventScope;
void resetOnEachMicrotaskTick();
@@ -446,17 +447,15 @@ public:
// To do that, we count the number of times we register a module.
int napiModuleRegisterCallCount = 0;
// NAPI instance data
// This is not a correct implementation
// Addon modules can override each other's data
void* napiInstanceData = nullptr;
void* napiInstanceDataFinalizer = nullptr;
void* napiInstanceDataFinalizerHint = nullptr;
// Used by napi_wrap to associate native objects with JS values.
// Should only use JSCell* keys and NapiExternal values that contain NapiRefs.
LazyProperty<JSGlobalObject, JSC::JSWeakMap> m_napiWraps;
// Used by napi_type_tag_object to associate a 128-bit type ID with JS objects.
// Should only use JSCell* keys and NapiTypeTag values.
LazyProperty<JSGlobalObject, JSC::JSWeakMap> m_napiTypeTags;
JSC::JSWeakMap* napiWraps() const { return m_napiWraps.getInitializedOnMainThread(this); }
JSC::JSWeakMap* napiTypeTags() const { return m_napiTypeTags.getInitializedOnMainThread(this); }
Bun::JSMockModule mockModule;
@@ -557,7 +556,6 @@ public:
LazyProperty<JSGlobalObject, Structure> m_JSCryptoKey;
LazyProperty<JSGlobalObject, Structure> m_NapiExternalStructure;
LazyProperty<JSGlobalObject, Structure> m_NapiPrototypeStructure;
LazyProperty<JSGlobalObject, Structure> m_NAPIFunctionStructure;
LazyProperty<JSGlobalObject, Structure> m_NapiHandleScopeImplStructure;
LazyProperty<JSGlobalObject, Structure> m_NapiTypeTagStructure;
@@ -573,6 +571,11 @@ public:
bool hasOverridenModuleResolveFilenameFunction = false;
WTF::Vector<std::unique_ptr<napi_env__>> m_napiEnvs;
napi_env makeNapiEnv(const napi_module&);
napi_env makeNapiEnvForFFI();
bool hasNapiFinalizers() const;
private:
DOMGuardedObjectSet m_guardedObjects WTF_GUARDED_BY_LOCK(m_gcLock);
WebCore::SubtleCrypto* m_subtleCrypto = nullptr;

View File

@@ -3599,7 +3599,7 @@ bool JSC__JSValue__isUndefinedOrNull(JSC__JSValue JSValue0)
JSC__JSValue JSC__JSValue__jsBoolean(bool arg0)
{
return JSC::JSValue::encode(JSC::jsBoolean(arg0));
};
}
JSC__JSValue JSC__JSValue__jsDoubleNumber(double arg0)
{
return JSC::JSValue::encode(JSC::jsNumber(arg0));
@@ -3608,32 +3608,35 @@ JSC__JSValue JSC__JSValue__jsDoubleNumber(double arg0)
JSC__JSValue JSC__JSValue__jsEmptyString(JSC__JSGlobalObject* arg0)
{
return JSC::JSValue::encode(JSC::jsEmptyString(arg0->vm()));
};
JSC__JSValue JSC__JSValue__jsNull() { return JSC::JSValue::encode(JSC::jsNull()); };
}
JSC__JSValue JSC__JSValue__jsNull()
{
return JSC::JSValue::encode(JSC::jsNull());
}
JSC__JSValue JSC__JSValue__jsNumberFromChar(unsigned char arg0)
{
return JSC::JSValue::encode(JSC::jsNumber(arg0));
};
}
JSC__JSValue JSC__JSValue__jsNumberFromDouble(double arg0)
{
return JSC::JSValue::encode(JSC::jsNumber(arg0));
};
}
JSC__JSValue JSC__JSValue__jsNumberFromInt32(int32_t arg0)
{
return JSC::JSValue::encode(JSC::jsNumber(arg0));
};
}
JSC__JSValue JSC__JSValue__jsNumberFromInt64(int64_t arg0)
{
return JSC::JSValue::encode(JSC::jsNumber(arg0));
};
}
JSC__JSValue JSC__JSValue__jsNumberFromU16(uint16_t arg0)
{
return JSC::JSValue::encode(JSC::jsNumber(arg0));
};
}
JSC__JSValue JSC__JSValue__jsNumberFromUint64(uint64_t arg0)
{
return JSC::JSValue::encode(JSC::jsNumber(arg0));
};
}
int64_t JSC__JSValue__toInt64(JSC__JSValue val)
{

View File

@@ -23,6 +23,7 @@ const String = bun.String;
const ErrorableString = JSC.ErrorableString;
const JSError = bun.JSError;
const OOM = bun.OOM;
const napi = @import("../../napi/napi.zig");
pub const JSObject = extern struct {
pub const shim = Shimmer("JSC", "JSObject", @This());
@@ -3417,6 +3418,12 @@ pub const JSGlobalObject = opaque {
return ZigGlobalObject__readableStreamToFormData(this, value, content_type);
}
extern fn ZigGlobalObject__makeNapiEnvForFFI(*JSGlobalObject) *napi.NapiEnv;
pub fn makeNapiEnvForFFI(this: *JSGlobalObject) *napi.NapiEnv {
return ZigGlobalObject__makeNapiEnvForFFI(this);
}
pub inline fn assertOnJSThread(this: *JSGlobalObject) void {
if (bun.Environment.allow_assert) this.bunVM().assertOnJSThread();
}
@@ -3878,6 +3885,8 @@ pub const JSValue = enum(i64) {
.Float32Array => .kJSTypedArrayTypeFloat32Array,
.Float64Array => .kJSTypedArrayTypeFloat64Array,
.ArrayBuffer => .kJSTypedArrayTypeArrayBuffer,
.BigInt64Array => .kJSTypedArrayTypeBigInt64Array,
.BigUint64Array => .kJSTypedArrayTypeBigUint64Array,
// .DataView => .kJSTypedArrayTypeDataView,
else => .kJSTypedArrayTypeNone,
};

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,301 @@
#include "headers-handwritten.h"
#include "BunClientData.h"
#include <JavaScriptCore/CallFrame.h>
#include "js_native_api.h"
#include "node_api.h"
#include <JavaScriptCore/JSWeakValue.h>
#include "JSFFIFunction.h"
#include "ZigGlobalObject.h"
#include "napi_handle_scope.h"
#include "napi_finalizer.h"
#include "wtf/Assertions.h"
#include "napi_macros.h"
#include <list>
#include <unordered_set>
extern "C" void napi_internal_register_cleanup_zig(napi_env env);
extern "C" void napi_internal_crash_in_gc(napi_env);
extern "C" void Bun__crashHandler(const char* message, size_t message_len);
namespace Napi {
struct AsyncCleanupHook {
napi_async_cleanup_hook function = nullptr;
void* data = nullptr;
napi_async_cleanup_hook_handle handle = nullptr;
};
}
struct napi_async_cleanup_hook_handle__ {
napi_env env;
std::list<Napi::AsyncCleanupHook>::iterator iter;
napi_async_cleanup_hook_handle__(napi_env env, decltype(iter) iter)
: env(env)
, iter(iter)
{
}
};
#define NAPI_ABORT(message) Bun__crashHandler(message "", sizeof(message "") - 1)
#define NAPI_PERISH(...) \
do { \
WTFReportError(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__); \
WTFReportBacktrace(); \
NAPI_ABORT("Aborted"); \
} while (0)
#define NAPI_RELEASE_ASSERT(assertion, ...) \
do { \
if (UNLIKELY(!(assertion))) { \
WTFReportAssertionFailureWithMessage(__FILE__, __LINE__, __PRETTY_FUNCTION__, #assertion, __VA_ARGS__); \
WTFReportBacktrace(); \
NAPI_ABORT("Aborted"); \
} \
} while (0)
// Named this way so we can manipulate napi_env values directly (since napi_env is defined as a pointer to struct napi_env__)
struct napi_env__ {
public:
napi_env__(Zig::GlobalObject* globalObject, const napi_module& napiModule)
: m_globalObject(globalObject)
, m_napiModule(napiModule)
{
napi_internal_register_cleanup_zig(this);
}
~napi_env__()
{
delete[] filename;
}
void cleanup()
{
while (!m_cleanupHooks.empty()) {
auto [function, data] = m_cleanupHooks.back();
m_cleanupHooks.pop_back();
ASSERT(function != nullptr);
function(data);
}
while (!m_asyncCleanupHooks.empty()) {
auto [function, data, handle] = m_asyncCleanupHooks.back();
ASSERT(function != nullptr);
function(handle, data);
delete handle;
m_asyncCleanupHooks.pop_back();
}
m_isFinishingFinalizers = true;
for (const BoundFinalizer& boundFinalizer : m_finalizers) {
Bun::NapiHandleScope handle_scope(m_globalObject);
boundFinalizer.call(this);
}
m_finalizers.clear();
m_isFinishingFinalizers = false;
instanceDataFinalizer.call(this, instanceData, true);
instanceDataFinalizer.clear();
}
void removeFinalizer(napi_finalize callback, void* hint, void* data)
{
m_finalizers.erase({ callback, hint, data });
}
struct BoundFinalizer;
void removeFinalizer(const BoundFinalizer& finalizer)
{
m_finalizers.erase(finalizer);
}
const auto& addFinalizer(napi_finalize callback, void* hint, void* data)
{
return *m_finalizers.emplace(callback, hint, data).first;
}
bool hasFinalizers() const
{
return !m_finalizers.empty();
}
/// Will abort the process if a duplicate entry would be added.
void addCleanupHook(void (*function)(void*), void* data)
{
for (const auto& [existing_function, existing_data] : m_cleanupHooks) {
NAPI_RELEASE_ASSERT(function != existing_function || data != existing_data, "Attempted to add a duplicate NAPI environment cleanup hook");
}
m_cleanupHooks.emplace_back(function, data);
}
void removeCleanupHook(void (*function)(void*), void* data)
{
for (auto iter = m_cleanupHooks.begin(), end = m_cleanupHooks.end(); iter != end; ++iter) {
if (iter->first == function && iter->second == data) {
m_cleanupHooks.erase(iter);
return;
}
}
NAPI_PERISH("Attempted to remove a NAPI environment cleanup hook that had never been added");
}
napi_async_cleanup_hook_handle addAsyncCleanupHook(napi_async_cleanup_hook function, void* data)
{
for (const auto& [existing_function, existing_data, existing_handle] : m_asyncCleanupHooks) {
NAPI_RELEASE_ASSERT(function != existing_function || data != existing_data, "Attempted to add a duplicate async NAPI environment cleanup hook");
}
auto iter = m_asyncCleanupHooks.emplace(m_asyncCleanupHooks.end(), function, data);
iter->handle = new napi_async_cleanup_hook_handle__(this, iter);
return iter->handle;
}
void removeAsyncCleanupHook(napi_async_cleanup_hook_handle handle)
{
for (const auto& [existing_function, existing_data, existing_handle] : m_asyncCleanupHooks) {
if (existing_handle == handle) {
m_asyncCleanupHooks.erase(handle->iter);
delete handle;
return;
}
}
NAPI_PERISH("Attempted to remove an async NAPI environment cleanup hook that had never been added");
}
bool inGC() const
{
JSC::VM& vm = m_globalObject->vm();
return vm.isCollectorBusyOnCurrentThread();
}
void checkGC() const
{
NAPI_RELEASE_ASSERT(!inGC(),
"Attempted to call a non-GC-safe function inside a NAPI finalizer from a NAPI module with version %d.\n"
"Finalizers must not create new objects during garbage collection. Use the `node_api_post_finalizer` function\n"
"inside the finalizer to defer the code to the next event loop tick.\n",
m_napiModule.nm_version);
}
bool isVMTerminating() const
{
return m_globalObject->vm().hasTerminationRequest();
}
void doFinalizer(napi_finalize finalize_cb, void* data, void* finalize_hint)
{
if (!finalize_cb) {
return;
}
if (mustDeferFinalizers() && inGC()) {
napi_internal_enqueue_finalizer(this, finalize_cb, data, finalize_hint);
} else {
finalize_cb(this, data, finalize_hint);
}
}
inline Zig::GlobalObject* globalObject() const { return m_globalObject; }
inline const napi_module& napiModule() const { return m_napiModule; }
// Returns true if finalizers from this module need to be scheduled for the next tick after garbage collection, instead of running during garbage collection
inline bool mustDeferFinalizers() const
{
// Even when we'd normally have to defer the finalizer, if this is happening during the VM's last chance to finalize,
// we can't defer the finalizer and have to call it now.
return m_napiModule.nm_version != NAPI_VERSION_EXPERIMENTAL && !isVMTerminating();
}
inline bool isFinishingFinalizers() const { return m_isFinishingFinalizers; }
// Almost all NAPI functions should set error_code to the status they're returning right before
// they return it
napi_extended_error_info m_lastNapiErrorInfo = {
.error_message = "",
// Not currently used by Bun -- always nullptr
.engine_reserved = nullptr,
// Not currently used by Bun -- always zero
.engine_error_code = 0,
.error_code = napi_ok,
};
void* instanceData = nullptr;
Bun::NapiFinalizer instanceDataFinalizer;
char* filename = nullptr;
struct BoundFinalizer {
napi_finalize callback = nullptr;
void* hint = nullptr;
void* data = nullptr;
// Allows bound finalizers to effectively remove themselves during cleanup without breaking iteration.
// Safe to be mutable because it's not included in the hash.
mutable bool active = true;
BoundFinalizer() = default;
BoundFinalizer(const Bun::NapiFinalizer& finalizer, void* data)
: callback(finalizer.callback())
, hint(finalizer.hint())
, data(data)
{
}
BoundFinalizer(napi_finalize callback, void* hint, void* data)
: callback(callback)
, hint(hint)
, data(data)
{
}
void call(napi_env env) const
{
if (callback && active) {
callback(env, data, hint);
}
}
void deactivate(napi_env env) const
{
if (env->isFinishingFinalizers()) {
active = false;
} else {
env->removeFinalizer(*this);
// At this point the BoundFinalizer has been destroyed, but because we're not doing anything else here it's safe.
// https://isocpp.org/wiki/faq/freestore-mgmt#delete-this
}
}
bool operator==(const BoundFinalizer& other) const
{
return this == &other || (callback == other.callback && hint == other.hint && data == other.data);
}
struct Hash {
std::size_t operator()(const napi_env__::BoundFinalizer& bound) const
{
constexpr std::hash<void*> hasher;
constexpr std::ptrdiff_t magic = 0x9e3779b9;
return (hasher(reinterpret_cast<void*>(bound.callback)) + magic) ^ (hasher(bound.hint) + magic) ^ (hasher(bound.data) + magic);
}
};
};
private:
Zig::GlobalObject* m_globalObject = nullptr;
napi_module m_napiModule;
// TODO(@heimskr): Use WTF::HashSet
std::unordered_set<BoundFinalizer, BoundFinalizer::Hash> m_finalizers;
bool m_isFinishingFinalizers = false;
std::list<std::pair<void (*)(void*), void*>> m_cleanupHooks;
std::list<Napi::AsyncCleanupHook> m_asyncCleanupHooks;
};
extern "C" void napi_internal_cleanup_env_cpp(napi_env);
extern "C" void napi_internal_remove_finalizer(napi_env, napi_finalize callback, void* hint, void* data);
namespace JSC {
class JSGlobalObject;
@@ -20,10 +310,33 @@ class JSSourceCode;
namespace Napi {
JSC::SourceCode generateSourceCode(WTF::String keyString, JSC::VM& vm, JSC::JSObject* object, JSC::JSGlobalObject* globalObject);
class NapiRefWeakHandleOwner final : public JSC::WeakHandleOwner {
public:
// Equivalent to v8impl::Ownership::kUserland
void finalize(JSC::Handle<JSC::Unknown>, void* context) final;
static NapiRefWeakHandleOwner& weakValueHandleOwner()
{
static NeverDestroyed<NapiRefWeakHandleOwner> jscWeakValueHandleOwner;
return jscWeakValueHandleOwner;
}
};
class NapiRefSelfDeletingWeakHandleOwner final : public JSC::WeakHandleOwner {
public:
// Equivalent to v8impl::Ownership::kRuntime
void finalize(JSC::Handle<JSC::Unknown>, void* context) final;
static NapiRefSelfDeletingWeakHandleOwner& weakValueHandleOwner()
{
static NeverDestroyed<NapiRefSelfDeletingWeakHandleOwner> jscWeakValueHandleOwner;
return jscWeakValueHandleOwner;
}
};
}
namespace Zig {
using namespace JSC;
static inline JSValue toJS(napi_value val)
@@ -33,7 +346,7 @@ static inline JSValue toJS(napi_value val)
static inline Zig::GlobalObject* toJS(napi_env val)
{
return reinterpret_cast<Zig::GlobalObject*>(val);
return val->globalObject();
}
static inline napi_value toNapi(JSC::JSValue val, Zig::GlobalObject* globalObject)
@@ -46,19 +359,6 @@ static inline napi_value toNapi(JSC::JSValue val, Zig::GlobalObject* globalObjec
return reinterpret_cast<napi_value>(JSC::JSValue::encode(val));
}
static inline napi_env toNapi(JSC::JSGlobalObject* val)
{
return reinterpret_cast<napi_env>(val);
}
class NapiFinalizer {
public:
void* finalize_hint = nullptr;
napi_finalize finalize_cb;
void call(JSC::JSGlobalObject* globalObject, void* data);
};
// This is essentially JSC::JSWeakValue, except with a JSCell* instead of a
// JSObject*. Sometimes, a napi embedder might want to store a JSC::Exception, a
// JSC::HeapBigInt, JSC::Symbol, etc inside of a NapiRef. So we can't limit it
@@ -147,16 +447,20 @@ public:
void unref();
void clear();
NapiRef(JSC::JSGlobalObject* global, uint32_t count)
NapiRef(napi_env env, uint32_t count, Bun::NapiFinalizer finalizer)
: env(env)
, globalObject(JSC::Weak<JSC::JSGlobalObject>(env->globalObject()))
, finalizer(WTFMove(finalizer))
, refCount(count)
{
globalObject = JSC::Weak<JSC::JSGlobalObject>(global);
strongRef = {};
weakValueRef.clear();
refCount = count;
}
JSC::JSValue value() const
{
if (m_isEternal) {
return m_eternalGlobalSymbolRef.get();
}
if (refCount == 0) {
return weakValueRef.get();
}
@@ -164,20 +468,63 @@ public:
return strongRef.get();
}
void setValueInitial(JSC::JSValue value, bool can_be_weak)
{
if (refCount > 0) {
strongRef.set(globalObject->vm(), value);
}
// In NAPI non-experimental, types other than object, function and symbol can't be used as values for references.
// In NAPI experimental, they can be, but we must not store weak references to them.
if (can_be_weak) {
weakValueRef.set(value, Napi::NapiRefWeakHandleOwner::weakValueHandleOwner(), this);
}
if (value.isSymbol()) {
auto* symbol = jsDynamicCast<JSC::Symbol*>(value);
ASSERT(symbol != nullptr);
if (symbol->uid().isRegistered()) {
// Global symbols must always be retrievable,
// even if garbage collection happens while the ref count is 0.
m_eternalGlobalSymbolRef.set(globalObject->vm(), symbol);
m_isEternal = true;
}
}
}
void callFinalizer()
{
finalizer.call(env, nativeObject, !env->mustDeferFinalizers() || !env->inGC());
finalizer.clear();
}
~NapiRef()
{
NAPI_LOG("destruct napi ref %p", this);
if (boundCleanup) {
boundCleanup->deactivate(env);
boundCleanup = nullptr;
}
strongRef.clear();
// The weak ref can lead to calling the destructor
// so we must first clear the weak ref before we call the finalizer
weakValueRef.clear();
}
napi_env env = nullptr;
JSC::Weak<JSC::JSGlobalObject> globalObject;
NapiWeakValue weakValueRef;
JSC::Strong<JSC::Unknown> strongRef;
NapiFinalizer finalizer;
void* data = nullptr;
Bun::NapiFinalizer finalizer;
const napi_env__::BoundFinalizer* boundCleanup = nullptr;
void* nativeObject = nullptr;
uint32_t refCount = 0;
bool releaseOnWeaken = false;
private:
JSC::Strong<JSC::Symbol> m_eternalGlobalSymbolRef;
bool m_isEternal = false;
};
static inline napi_ref toNapi(NapiRef* val)
@@ -210,8 +557,7 @@ public:
DECLARE_EXPORT_INFO;
JS_EXPORT_PRIVATE static NapiClass* create(VM&, Zig::GlobalObject*, const char* utf8name,
size_t length,
JS_EXPORT_PRIVATE static NapiClass* create(VM&, napi_env, WTF::String name,
napi_callback constructor,
void* data,
size_t property_count,
@@ -223,25 +569,28 @@ public:
return Structure::create(vm, globalObject, prototype, TypeInfo(JSFunctionType, StructureFlags), info());
}
CFFIFunction constructor()
{
return m_constructor;
}
void* dataPtr = nullptr;
CFFIFunction m_constructor = nullptr;
NapiRef* napiRef = nullptr;
inline napi_callback constructor() const { return m_constructor; }
inline void*& dataPtr() { return m_dataPtr; }
inline void* const& dataPtr() const { return m_dataPtr; }
inline napi_env env() const { return m_env; }
private:
NapiClass(VM& vm, NativeExecutable* executable, JSC::JSGlobalObject* global, Structure* structure)
: Base(vm, executable, global, structure)
NapiClass(VM& vm, NativeExecutable* executable, napi_env env, Structure* structure, void* data)
: Base(vm, executable, env->globalObject(), structure)
, m_dataPtr(data)
, m_env(env)
{
}
void finishCreation(VM&, NativeExecutable*, unsigned length, const String& name, napi_callback constructor,
void finishCreation(VM&, NativeExecutable*, const String& name, napi_callback constructor,
void* data,
size_t property_count,
const napi_property_descriptor* properties);
void* m_dataPtr = nullptr;
napi_callback m_constructor = nullptr;
napi_env m_env = nullptr;
DECLARE_VISIT_CHILDREN;
};
@@ -275,27 +624,14 @@ public:
NapiPrototype* subclass(JSC::JSGlobalObject* globalObject, JSC::JSObject* newTarget)
{
auto& vm = this->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* targetFunction = jsCast<JSFunction*>(newTarget);
FunctionRareData* rareData = targetFunction->ensureRareData(vm);
auto* prototype = newTarget->get(globalObject, vm.propertyNames->prototype).getObject();
RETURN_IF_EXCEPTION(scope, nullptr);
// This must be kept in-sync with InternalFunction::createSubclassStructure
Structure* structure = rareData->internalFunctionAllocationStructure();
if (UNLIKELY(!(structure && structure->classInfoForCells() == this->structure()->classInfoForCells() && structure->globalObject() == globalObject))) {
structure = rareData->createInternalFunctionAllocationStructureFromBase(vm, globalObject, prototype, this->structure());
VM& vm = globalObject->vm();
Structure* structure = JSC::InternalFunction::createSubclassStructure(globalObject, newTarget, this->structure());
if (!structure) {
return nullptr;
}
RETURN_IF_EXCEPTION(scope, nullptr);
NapiPrototype* footprint = new (NotNull, allocateCell<NapiPrototype>(vm)) NapiPrototype(vm, structure);
footprint->finishCreation(vm);
RELEASE_AND_RETURN(scope, footprint);
return NapiPrototype::create(vm, structure);
}
NapiRef* napiRef = nullptr;
private:
NapiPrototype(VM& vm, Structure* structure)
: Base(vm, structure)
@@ -308,6 +644,4 @@ static inline NapiRef* toJS(napi_ref val)
return reinterpret_cast<NapiRef*>(val);
}
Structure* createNAPIFunctionStructure(VM& vm, JSC::JSGlobalObject* globalObject);
}

View File

@@ -5,11 +5,8 @@ namespace Bun {
NapiExternal::~NapiExternal()
{
if (finalizer) {
// We cannot call globalObject() here because it is in a finalizer.
// https://github.com/oven-sh/bun/issues/13001#issuecomment-2290022312
reinterpret_cast<napi_finalize>(finalizer)(toNapi(this->napi_env), m_value, m_finalizerHint);
}
ASSERT(m_env);
m_finalizer.call(m_env, m_value, !m_env->mustDeferFinalizers());
}
void NapiExternal::destroy(JSC::JSCell* cell)

View File

@@ -2,10 +2,11 @@
#pragma once
#include "napi_finalizer.h"
#include "root.h"
#include "BunBuiltinNames.h"
#include "BunClientData.h"
#include "napi.h"
namespace Bun {
@@ -47,11 +48,11 @@ public:
JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, void* finalizer)
static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, napi_env env, napi_finalize callback)
{
NapiExternal* accessor = new (NotNull, JSC::allocateCell<NapiExternal>(vm)) NapiExternal(vm, structure);
accessor->finishCreation(vm, value, finalizer_hint, finalizer);
accessor->finishCreation(vm, value, finalizer_hint, env, callback);
#if BUN_DEBUG
if (auto* callFrame = vm.topCallFrame) {
@@ -75,13 +76,12 @@ public:
return accessor;
}
void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, void* finalizer)
void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, napi_env env, napi_finalize callback)
{
Base::finishCreation(vm);
m_value = value;
m_finalizerHint = finalizer_hint;
napi_env = this->globalObject();
this->finalizer = finalizer;
m_env = env;
m_finalizer = NapiFinalizer { callback, finalizer_hint };
}
static void destroy(JSC::JSCell* cell);
@@ -89,9 +89,8 @@ public:
void* value() const { return m_value; }
void* m_value;
void* m_finalizerHint;
void* finalizer;
JSGlobalObject* napi_env;
NapiFinalizer m_finalizer;
napi_env m_env;
#if BUN_DEBUG
String sourceOriginURL = String();

View File

@@ -0,0 +1,26 @@
#include "napi_finalizer.h"
#include "napi.h"
#include "napi_macros.h"
namespace Bun {
void NapiFinalizer::call(napi_env env, void* data, bool immediate)
{
if (m_callback) {
NAPI_LOG_CURRENT_FUNCTION;
if (immediate) {
m_callback(env, data, m_hint);
} else {
napi_internal_enqueue_finalizer(env, m_callback, data, m_hint);
}
}
}
void NapiFinalizer::clear()
{
m_callback = nullptr;
m_hint = nullptr;
}
} // namespace Bun

View File

@@ -0,0 +1,31 @@
#pragma once
#include "root.h"
#include "js_native_api.h"
extern "C" void napi_internal_enqueue_finalizer(napi_env env, napi_finalize finalize_cb, void* data, void* hint);
namespace Bun {
class NapiFinalizer {
public:
NapiFinalizer(napi_finalize callback, void* hint)
: m_callback(callback)
, m_hint(hint)
{
}
NapiFinalizer() = default;
void call(napi_env env, void* data, bool immediate = false);
void clear();
inline napi_finalize callback() const { return m_callback; }
inline void* hint() const { return m_hint; }
private:
napi_finalize m_callback = nullptr;
void* m_hint = nullptr;
};
} // namespace Bun

View File

@@ -1,4 +1,5 @@
#include "napi_handle_scope.h"
#include "napi.h"
#include "ZigGlobalObject.h"
@@ -80,6 +81,7 @@ NapiHandleScopeImpl::Slot* NapiHandleScopeImpl::reserveSlot()
NapiHandleScopeImpl* NapiHandleScope::open(Zig::GlobalObject* globalObject, bool escapable)
{
NAPI_LOG_CURRENT_FUNCTION;
auto& vm = globalObject->vm();
// Do not create a new handle scope while a finalizer is in progress
// This state is possible because we call napi finalizers immediately
@@ -103,6 +105,7 @@ NapiHandleScopeImpl* NapiHandleScope::open(Zig::GlobalObject* globalObject, bool
void NapiHandleScope::close(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current)
{
NAPI_LOG_CURRENT_FUNCTION;
// napi handle scopes may be null pointers if created inside a finalizer
if (!current) {
return;
@@ -127,19 +130,19 @@ NapiHandleScope::~NapiHandleScope()
NapiHandleScope::close(m_globalObject, m_impl);
}
extern "C" NapiHandleScopeImpl* NapiHandleScope__open(Zig::GlobalObject* globalObject, bool escapable)
extern "C" NapiHandleScopeImpl* NapiHandleScope__open(napi_env env, bool escapable)
{
return NapiHandleScope::open(globalObject, escapable);
return NapiHandleScope::open(env->globalObject(), escapable);
}
extern "C" void NapiHandleScope__close(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current)
extern "C" void NapiHandleScope__close(napi_env env, NapiHandleScopeImpl* current)
{
return NapiHandleScope::close(globalObject, current);
return NapiHandleScope::close(env->globalObject(), current);
}
extern "C" void NapiHandleScope__append(Zig::GlobalObject* globalObject, JSC::EncodedJSValue value)
extern "C" void NapiHandleScope__append(napi_env env, JSC::EncodedJSValue value)
{
globalObject->m_currentNapiHandleScopeImpl.get()->append(JSC::JSValue::decode(value));
env->globalObject()->m_currentNapiHandleScopeImpl.get()->append(JSC::JSValue::decode(value));
}
extern "C" bool NapiHandleScope__escape(NapiHandleScopeImpl* handleScope, JSC::EncodedJSValue value)

View File

@@ -3,6 +3,8 @@
#include "BunClientData.h"
#include "root.h"
typedef struct napi_env__* napi_env;
namespace Bun {
// An array of write barriers (so that newly-added objects are not lost by GC) to JSValues. Unlike
@@ -80,14 +82,14 @@ private:
};
// Create a new handle scope in the given environment
extern "C" NapiHandleScopeImpl* NapiHandleScope__open(Zig::GlobalObject* globalObject, bool escapable);
extern "C" NapiHandleScopeImpl* NapiHandleScope__open(napi_env env, bool escapable);
// Pop the most recently created handle scope in the given environment and restore the old one.
// Asserts that `current` is the active handle scope.
extern "C" void NapiHandleScope__close(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current);
extern "C" void NapiHandleScope__close(napi_env env, NapiHandleScopeImpl* current);
// Store a value in the active handle scope in the given environment
extern "C" void NapiHandleScope__append(Zig::GlobalObject* globalObject, JSC::EncodedJSValue value);
extern "C" void NapiHandleScope__append(napi_env env, JSC::EncodedJSValue value);
// Put a value from the current handle scope into its escape slot reserved in the outer handle
// scope. Returns false if the current handle scope is not escapable or if escape has already been

View File

@@ -0,0 +1,30 @@
#pragma once
#define NAPI_VERBOSE 0
#if NAPI_VERBOSE
#include <stdio.h>
#include <stdarg.h>
#if defined __has_attribute
#if __has_attribute(__format__)
__attribute__((__format__(__printf__, 4, 5))) static inline void napi_log(const char* file, long line, const char* function, const char* fmt, ...)
#endif
#endif
{
printf("[%s:%ld] %s: ", file, line, function);
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
printf("\n");
}
#define NAPI_LOG_CURRENT_FUNCTION printf("[%s:%d] %s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__)
#define NAPI_LOG(fmt, ...) napi_log(__FILE__, __LINE__, __PRETTY_FUNCTION__, fmt __VA_OPT__(, ) __VA_ARGS__)
#else
#define NAPI_LOG_CURRENT_FUNCTION
#define NAPI_LOG(fmt, ...)
#endif

View File

@@ -12,7 +12,8 @@ Local<External> External::New(Isolate* isolate, void* value)
auto globalObject = isolate->globalObject();
auto& vm = globalObject->vm();
auto structure = globalObject->NapiExternalStructure();
Bun::NapiExternal* val = Bun::NapiExternal::create(vm, structure, value, nullptr, nullptr);
Bun::NapiExternal* val = Bun::NapiExternal::create(vm, structure, value,
nullptr /* hint */, nullptr /* env */, nullptr /* callback */);
return isolate->currentHandleScope()->createLocal<External>(vm, val);
}

View File

@@ -71,6 +71,7 @@ JSC::EncodedJSValue FunctionTemplate::functionCall(JSC::JSGlobalObject* globalOb
// object.
JSC::JSObject* jscThis = globalObject->globalThis();
if (!callFrame->thisValue().isUndefinedOrNull()) {
// TODO(@190n) throwscope, assert no exception
jscThis = callFrame->thisValue().toObject(globalObject);
}
Local<Object> thisObject = hs.createLocal<Object>(vm, jscThis);

View File

@@ -57,7 +57,7 @@ EventTarget* JSEventTarget::toWrapped(VM& vm, JSValue value)
// if (value.inherits<JSDOMWindow>())
// return &jsCast<JSDOMWindow*>(asObject(value))->wrapped();
if (value.inherits<JSDOMGlobalObject>())
return &jsCast<JSDOMGlobalObject*>(asObject(value))->globalEventScope;
return jsCast<JSDOMGlobalObject*>(asObject(value))->globalEventScope.ptr();
if (value.inherits<JSEventTarget>())
return &jsCast<JSEventTarget*>(asObject(value))->wrapped();
return nullptr;

View File

@@ -64,7 +64,7 @@ MessagePortChannelProvider& MessagePortChannelProvider::fromContext(ScriptExecut
// if (auto workletScope = dynamicDowncast<WorkletGlobalScope>(context))
// return workletScope->messagePortChannelProvider();
return jsCast<Zig::GlobalObject*>(context.jsGlobalObject())->globalEventScope.messagePortChannelProvider();
return jsCast<Zig::GlobalObject*>(context.jsGlobalObject())->globalEventScope->messagePortChannelProvider();
}
} // namespace WebCore

View File

@@ -243,7 +243,7 @@ ExceptionOr<void> Worker::postMessage(JSC::JSGlobalObject& state, JSC::JSValue m
auto ports = MessagePort::entanglePorts(context, WTFMove(message.transferredPorts));
auto event = MessageEvent::create(*globalObject, message.message.releaseNonNull(), std::nullopt, WTFMove(ports));
globalObject->globalEventScope.dispatchEvent(event.event);
globalObject->globalEventScope->dispatchEvent(event.event);
});
return {};
}
@@ -344,9 +344,9 @@ void Worker::dispatchOnline(Zig::GlobalObject* workerGlobalObject)
return;
}
RELEASE_ASSERT(&thisContext->vm() == &workerGlobalObject->vm());
RELEASE_ASSERT(thisContext == workerGlobalObject->globalEventScope.scriptExecutionContext());
RELEASE_ASSERT(thisContext == workerGlobalObject->globalEventScope->scriptExecutionContext());
if (workerGlobalObject->globalEventScope.hasActiveEventListeners(eventNames().messageEvent)) {
if (workerGlobalObject->globalEventScope->hasActiveEventListeners(eventNames().messageEvent)) {
auto tasks = std::exchange(this->m_pendingTasks, {});
lock.unlockEarly();
for (auto& task : tasks) {
@@ -446,7 +446,7 @@ extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, Worker
init.cancelable = false;
init.bubbles = false;
globalObject->globalEventScope.dispatchEvent(ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes));
globalObject->globalEventScope->dispatchEvent(ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes));
worker->dispatchError(message.toWTFString(BunString::ZeroCopy));
}

View File

@@ -770,6 +770,8 @@ pub const EventLoop = struct {
immediate_tasks: Queue = undefined,
next_immediate_tasks: Queue = undefined,
napi_finalizer_queue: JSC.napi.Finalizer.Queue = undefined,
concurrent_tasks: ConcurrentTask.Queue = ConcurrentTask.Queue{},
global: *JSGlobalObject = undefined,
virtual_machine: *JSC.VirtualMachine = undefined,
@@ -1275,6 +1277,7 @@ pub const EventLoop = struct {
pub fn tickImmediateTasks(this: *EventLoop, virtual_machine: *VirtualMachine) void {
_ = this.tickQueueWithCount(virtual_machine, "immediate_tasks");
JSC.napi.Finalizer.drain(&this.napi_finalizer_queue);
}
fn tickConcurrent(this: *EventLoop) void {

View File

@@ -428,6 +428,11 @@ pub export fn Bun__GlobalObject__hasIPC(global: *JSC.JSGlobalObject) bool {
}
extern fn Bun__Process__queueNextTick1(*JSC.ZigGlobalObject, JSC.JSValue, JSC.JSValue) void;
extern fn Bun__queueFinishNapiFinalizers(?*JSC.JSGlobalObject) bool;
pub export fn Bun__isNapiFinalizerQueueEmpty(globalObject: *JSGlobalObject) bool {
return globalObject.bunVM().eventLoop().napi_finalizer_queue.count == 0;
}
comptime {
const Bun__Process__send = JSC.toJSHostFunction(Bun__Process__send_);
@@ -1322,6 +1327,15 @@ pub const VirtualMachine = struct {
}
pub fn onBeforeExit(this: *VirtualMachine) void {
if (Bun__queueFinishNapiFinalizers(this.global) or this.eventLoop().napi_finalizer_queue.count > 0) {
// If we have any finalizers queued, we need to run the event loop until the finalizers are done.
// If there are no finalizers remaining, this isn't necessary.
while (this.isEventLoopAlive()) {
this.tick();
this.eventLoop().autoTickActive();
}
}
this.exit_handler.dispatchOnBeforeExit();
var dispatch = false;
while (true) {
@@ -1810,6 +1824,7 @@ pub const VirtualMachine = struct {
this.has_enabled_macro_mode = true;
this.macro_event_loop.tasks = EventLoop.Queue.init(default_allocator);
this.macro_event_loop.immediate_tasks = EventLoop.Queue.init(default_allocator);
this.macro_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
this.macro_event_loop.next_immediate_tasks = EventLoop.Queue.init(default_allocator);
this.macro_event_loop.tasks.ensureTotalCapacity(16) catch unreachable;
this.macro_event_loop.global = this.global;
@@ -1903,6 +1918,7 @@ pub const VirtualMachine = struct {
vm.regular_event_loop.immediate_tasks = EventLoop.Queue.init(
default_allocator,
);
vm.regular_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
vm.regular_event_loop.next_immediate_tasks = EventLoop.Queue.init(
default_allocator,
);
@@ -2006,6 +2022,7 @@ pub const VirtualMachine = struct {
vm.regular_event_loop.tasks = EventLoop.Queue.init(
default_allocator,
);
vm.regular_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
vm.regular_event_loop.immediate_tasks = EventLoop.Queue.init(
default_allocator,
);
@@ -2170,6 +2187,7 @@ pub const VirtualMachine = struct {
.debug_thread_id = if (Environment.allow_assert) std.Thread.getCurrentId() else {},
};
vm.source_mappings.init(&vm.saved_source_map_table);
vm.regular_event_loop.napi_finalizer_queue = JSC.napi.Finalizer.Queue.init(default_allocator);
vm.regular_event_loop.tasks = EventLoop.Queue.init(
default_allocator,
);

View File

@@ -46,6 +46,7 @@ pub const kJSTypeNumber = @intFromEnum(JSType.kJSTypeNumber);
pub const kJSTypeString = @intFromEnum(JSType.kJSTypeString);
pub const kJSTypeObject = @intFromEnum(JSType.kJSTypeObject);
pub const kJSTypeSymbol = @intFromEnum(JSType.kJSTypeSymbol);
/// From JSValueRef.h:81
pub const JSTypedArrayType = enum(c_uint) {
kJSTypedArrayTypeInt8Array,
kJSTypedArrayTypeInt16Array,
@@ -58,6 +59,8 @@ pub const JSTypedArrayType = enum(c_uint) {
kJSTypedArrayTypeFloat64Array,
kJSTypedArrayTypeArrayBuffer,
kJSTypedArrayTypeNone,
kJSTypedArrayTypeBigInt64Array,
kJSTypedArrayTypeBigUint64Array,
_,
};
pub const kJSTypedArrayTypeInt8Array = @intFromEnum(JSTypedArrayType.kJSTypedArrayTypeInt8Array);

View File

@@ -469,6 +469,7 @@ pub const WebWorker = struct {
/// Request a terminate (Called from main thread from worker.terminate(), or inside worker in process.exit())
/// The termination will actually happen after the next tick of the worker's loop.
pub fn requestTerminate(this: *WebWorker) callconv(.C) void {
// TODO(@heimskr): make WebWorker termination more immediate. Currently, console.log after process.exit will go through if in a WebWorker.
if (this.status.load(.acquire) == .terminated) {
return;
}

View File

@@ -310,7 +310,7 @@ pub fn crashHandler(
}
if (reason == .out_of_memory) {
writer.writeAll(
\\Bun has ran out of memory.
\\Bun has run out of memory.
\\
\\To send a redacted crash report to Bun's team,
\\please file a GitHub issue using the link below:
@@ -1425,7 +1425,9 @@ fn report(url: []const u8) void {
fn crash() noreturn {
switch (bun.Environment.os) {
.windows => {
std.posix.abort();
// This exit code is what Node.js uses when it calls
// abort. This is relied on by their Node-API tests.
bun.C.quick_exit(134);
},
else => {
// Install default handler so that the tkill below will terminate.
@@ -1780,3 +1782,11 @@ pub fn removePreCrashHandler(ptr: *anyopaque) void {
} else return;
_ = before_crash_handlers.orderedRemove(index);
}
export fn Bun__crashHandler(message_ptr: [*]u8, message_len: usize) noreturn {
crashHandler(.{ .panic = message_ptr[0..message_len] }, null, @returnAddress());
}
comptime {
_ = &Bun__crashHandler;
}

View File

@@ -93,12 +93,10 @@ typedef enum {
napi_would_deadlock // unused
} napi_status;
// Note: when adding a new enum value to `napi_status`, please also update
// * `const int last_status` in the definition of `napi_get_last_error_info()'
// in file js_native_api_v8.cc.
// * `const char* error_messages[]` in file js_native_api_v8.cc with a brief
// * `constexpr int last_status` in the definition of `napi_get_last_error_info()'
// in file napi.cpp.
// * `const char* error_messages[]` in file napi.cpp with a brief
// message explaining the error.
// * the definition of `napi_status` in doc/api/n-api.md to reflect the newly
// added value(s).
typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info);
typedef void (*napi_finalize)(napi_env env, void *finalize_data,

File diff suppressed because it is too large Load Diff

View File

@@ -544,7 +544,6 @@ EXPORTS
napi_reference_ref
napi_reference_unref
napi_get_reference_value
napi_get_reference_value_internal
napi_throw
napi_throw_error
napi_throw_type_error
@@ -567,6 +566,9 @@ EXPORTS
napi_is_detached_arraybuffer
napi_create_external_buffer
napi_fatal_exception
node_api_create_buffer_from_arraybuffer
node_api_get_module_file_name
node_api_post_finalizer
?TryGetCurrent@Isolate@v8@@SAPEAV12@XZ
?GetCurrent@Isolate@v8@@SAPEAV12@XZ
?GetCurrentContext@Isolate@v8@@QEAA?AV?$Local@VContext@v8@@@2@XZ

View File

@@ -149,6 +149,9 @@
_node_api_create_syntax_error;
_node_api_symbol_for;
_node_api_throw_syntax_error;
_node_api_create_buffer_from_arraybuffer;
_node_api_get_module_file_name;
_node_api_post_finalizer;
__ZN2v87Isolate10GetCurrentEv;
__ZN2v87Isolate13TryGetCurrentEv;
__ZN2v87Isolate17GetCurrentContextEv;

View File

@@ -148,6 +148,9 @@ _node_api_create_external_string_utf16
_node_api_create_syntax_error
_node_api_symbol_for
_node_api_throw_syntax_error
_node_api_create_buffer_from_arraybuffer
_node_api_get_module_file_name
_node_api_post_finalizer
__ZN2v87Isolate10GetCurrentEv
__ZN2v87Isolate13TryGetCurrentEv
__ZN2v87Isolate17GetCurrentContextEv

View File

@@ -535,7 +535,7 @@ pub const RwLock = if (@import("builtin").os.tag != .windows and @import("builti
pub fn deinit(self: *RwLock) void {
const safe_rc = switch (@import("builtin").os.tag) {
.dragonfly, .netbsd => std.posix.EAGAIN,
else => 0,
else => std.c.E.SUCCESS,
};
const rc = std.c.pthread_rwlock_destroy(&self.rwlock);
@@ -884,7 +884,7 @@ else if (@import("builtin").link_libc)
pub fn deinit(self: *Mutex) void {
const safe_rc = switch (@import("builtin").os.tag) {
.dragonfly, .netbsd => std.posix.EAGAIN,
else => 0,
else => std.c.E.SUCCESS,
};
const rc = std.c.pthread_mutex_destroy(&self.mutex);
@@ -1078,7 +1078,7 @@ else if (@import("builtin").link_libc)
pub fn deinit(self: *Condvar) void {
const safe_rc = switch (@import("builtin").os.tag) {
.dragonfly, .netbsd => std.posix.EAGAIN,
else => 0,
else => std.c.E.SUCCESS,
};
const rc = std.c.pthread_cond_destroy(&self.cond);

View File

@@ -135,6 +135,49 @@ async function cleanTestId(prisma: PrismaClient, testId: number) {
);
}
test(
"does not leak",
async (prisma: PrismaClient, _: number) => {
// prisma leak was 8 bytes per query, so a million requests would manifest as an 8MB leak
const batchSize = 1000;
const warmupIters = 1_000_000 / batchSize;
const testIters = 4_000_000 / batchSize;
const gcPeriod = 10_000 / batchSize;
let totalIters = 0;
async function runQuery() {
totalIters++;
// GC occasionally to make memory usage more deterministic
if (totalIters % gcPeriod == gcPeriod - 1) {
Bun.gc(true);
const line = `${totalIters},${(process.memoryUsage.rss() / 1024 / 1024) | 0}`;
// console.log(line);
// await appendFile("rss.csv", line + "\n");
}
const queries = [];
for (let i = 0; i < batchSize; i++) {
queries.push(prisma.$queryRaw`SELECT 1`);
}
await Promise.all(queries);
}
// warmup first
for (let i = 0; i < warmupIters; i++) {
await runQuery();
}
// measure memory now
const before = process.memoryUsage.rss();
// run a bunch more iterations to see if memory usage increases
for (let i = 0; i < testIters; i++) {
await runQuery();
}
const after = process.memoryUsage.rss();
const deltaMB = (after - before) / 1024 / 1024;
expect(deltaMB).toBeLessThan(10);
},
120_000,
);
test(
"CRUD basics",
async (prisma: PrismaClient, testId: number) => {

View File

@@ -0,0 +1,2 @@
CompileFlags:
CompilationDatabase: '.'

View File

@@ -0,0 +1,194 @@
#include "async_tests.h"
#include "utils.h"
#include <cassert>
#include <thread>
namespace napitests {
struct AsyncWorkData {
int result;
napi_deferred deferred;
napi_async_work work;
bool do_throw;
AsyncWorkData()
: result(0), deferred(nullptr), work(nullptr), do_throw(false) {}
static void execute(napi_env env, void *data) {
AsyncWorkData *async_work_data = reinterpret_cast<AsyncWorkData *>(data);
async_work_data->result = 42;
}
static void complete(napi_env c_env, napi_status status, void *data) {
Napi::Env env(c_env);
AsyncWorkData *async_work_data = reinterpret_cast<AsyncWorkData *>(data);
NODE_API_ASSERT_CUSTOM_RETURN(env, void(), status == napi_ok);
if (async_work_data->do_throw) {
// still have to resolve/reject otherwise the process times out
// we should not see the resolution as our unhandled exception handler
// exits the process before that can happen
napi_value result = env.Undefined();
NODE_API_CALL_CUSTOM_RETURN(
env, void(),
napi_resolve_deferred(env, async_work_data->deferred, result));
Napi::Error::New(env, "error from napi").ThrowAsJavaScriptException();
} else {
char buf[64] = {0};
snprintf(buf, sizeof(buf), "the number is %d", async_work_data->result);
napi_value result = Napi::String::New(env, buf);
NODE_API_CALL_CUSTOM_RETURN(
env, void(),
napi_resolve_deferred(env, async_work_data->deferred, result));
}
NODE_API_CALL_CUSTOM_RETURN(
env, void(), napi_delete_async_work(env, async_work_data->work));
delete async_work_data;
}
};
// create_promise(void *unused_run_gc_callback, bool do_throw): makes a promise
// using napi_Async_work that either resolves or throws in the complete callback
static napi_value create_promise(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
auto *data = new AsyncWorkData();
// info[0] is a callback to run the GC
data->do_throw = info[1].As<Napi::Boolean>();
napi_value promise;
NODE_API_CALL(env, napi_create_promise(env, &data->deferred, &promise));
napi_value resource_name =
Napi::String::New(env, "napitests__create_promise");
NODE_API_CALL(
env, napi_create_async_work(env, /* async resource */ nullptr,
resource_name, AsyncWorkData::execute,
AsyncWorkData::complete, data, &data->work));
NODE_API_CALL(env, napi_queue_async_work(env, data->work));
return promise;
}
class EchoWorker : public Napi::AsyncWorker {
public:
EchoWorker(Napi::Env env, Napi::Promise::Deferred deferred,
const std::string &&echo)
: Napi::AsyncWorker(env), m_echo(echo), m_deferred(deferred) {}
~EchoWorker() override {}
void Execute() override {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
void OnOK() override { m_deferred.Resolve(Napi::String::New(Env(), m_echo)); }
private:
std::string m_echo;
Napi::Promise::Deferred m_deferred;
};
static Napi::Value
create_promise_with_napi_cpp(const Napi::CallbackInfo &info) {
auto deferred = Napi::Promise::Deferred::New(info.Env());
auto *work = new EchoWorker(info.Env(), deferred, "hello world");
work->Queue();
return deferred.Promise();
}
struct ThreadsafeFunctionData {
napi_threadsafe_function tsfn;
napi_deferred deferred;
static void thread_entry(ThreadsafeFunctionData *data) {
using namespace std::chrono_literals;
std::this_thread::sleep_for(10ms);
// nonblocking means it will return an error if the threadsafe function's
// queue is full, which it should never do because we only use it once and
// we init with a capacity of 1
assert(napi_call_threadsafe_function(data->tsfn, nullptr,
napi_tsfn_nonblocking) == napi_ok);
}
static void tsfn_finalize_callback(napi_env env, void *finalize_data,
void *finalize_hint) {
printf("tsfn_finalize_callback\n");
ThreadsafeFunctionData *data =
reinterpret_cast<ThreadsafeFunctionData *>(finalize_data);
delete data;
}
static void tsfn_callback(napi_env c_env, napi_value js_callback,
void *context, void *data) {
// context == ThreadsafeFunctionData pointer
// data == nullptr
printf("tsfn_callback\n");
ThreadsafeFunctionData *tsfn_data =
reinterpret_cast<ThreadsafeFunctionData *>(context);
Napi::Env env(c_env);
napi_value recv = env.Undefined();
// call our JS function with undefined for this and no arguments
napi_value js_result;
napi_status call_result =
napi_call_function(env, recv, js_callback, 0, nullptr, &js_result);
NODE_API_ASSERT_CUSTOM_RETURN(env, void(),
call_result == napi_ok ||
call_result == napi_pending_exception);
if (call_result == napi_ok) {
// only resolve if js_callback did not return an error
// resolve the promise with the return value of the JS function
NODE_API_CALL_CUSTOM_RETURN(
env, void(),
napi_resolve_deferred(env, tsfn_data->deferred, js_result));
}
// clean up the threadsafe function
NODE_API_CALL_CUSTOM_RETURN(
env, void(),
napi_release_threadsafe_function(tsfn_data->tsfn, napi_tsfn_abort));
}
};
napi_value
create_promise_with_threadsafe_function(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
ThreadsafeFunctionData *tsfn_data = new ThreadsafeFunctionData;
napi_value async_resource_name = Napi::String::New(
env, "napitests::create_promise_with_threadsafe_function");
// this is called directly, without the GC callback, so argument 0 is a JS
// callback used to resolve the promise
NODE_API_CALL(env,
napi_create_threadsafe_function(
env, info[0], nullptr, async_resource_name,
// max_queue_size, initial_thread_count
1, 1,
// thread_finalize_data, thread_finalize_cb
tsfn_data, ThreadsafeFunctionData::tsfn_finalize_callback,
// context
tsfn_data, ThreadsafeFunctionData::tsfn_callback,
&tsfn_data->tsfn));
// create a promise we can return to JS and put the deferred counterpart in
// tsfn_data
napi_value promise;
NODE_API_CALL(env, napi_create_promise(env, &tsfn_data->deferred, &promise));
// spawn and release std::thread
std::thread secondary_thread(ThreadsafeFunctionData::thread_entry, tsfn_data);
secondary_thread.detach();
// return the promise to javascript
return promise;
}
void register_async_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, create_promise);
REGISTER_FUNCTION(env, exports, create_promise_with_napi_cpp);
REGISTER_FUNCTION(env, exports, create_promise_with_threadsafe_function);
}
} // namespace napitests

View File

@@ -0,0 +1,11 @@
#pragma once
// Tests that use napi_async_work or napi_deferred
#include "napi_with_version.h"
namespace napitests {
void register_async_tests(Napi::Env env, Napi::Object exports);
} // namespace napitests

View File

@@ -10,7 +10,7 @@
"AdditionalOptions": ["/std:c++20"],
},
},
"sources": ["main.cpp"],
"sources": ["main.cpp", "async_tests.cpp", "class_test.cpp", "conversion_tests.cpp", "js_test_helpers.cpp", "standalone_tests.cpp", "wrap_tests.cpp", "leak_tests.cpp"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
"libraries": [],
"dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],

View File

@@ -0,0 +1,171 @@
#include "class_test.h"
#include "utils.h"
namespace napitests {
static napi_value constructor(napi_env env, napi_callback_info info) {
napi_value this_value;
void *data;
NODE_API_CALL(
env, napi_get_cb_info(env, info, nullptr, nullptr, &this_value, &data));
printf("in constructor, data = \"%s\"\n",
reinterpret_cast<const char *>(data));
napi_value new_target;
NODE_API_CALL(env, napi_get_new_target(env, info, &new_target));
napi_value new_target_string;
NODE_API_CALL(env,
napi_coerce_to_string(env, new_target, &new_target_string));
char new_target_c_string[1024] = {0};
NODE_API_CALL(env, napi_get_value_string_utf8(
env, new_target_string, new_target_c_string,
sizeof new_target_c_string, nullptr));
// node and bun output different whitespace when stringifying a function,
// which we don't want the test to fail for
// so we attempt to delete everything in between {}
auto *open_brace = reinterpret_cast<char *>(
memchr(new_target_c_string, '{', sizeof new_target_c_string));
auto *close_brace = reinterpret_cast<char *>(
memchr(new_target_c_string, '}', sizeof new_target_c_string));
if (open_brace && close_brace && open_brace < close_brace) {
open_brace[1] = '}';
open_brace[2] = 0;
}
printf("new.target = %s\n", new_target_c_string);
printf("typeof this = %s\n",
napi_valuetype_to_string(get_typeof(env, this_value)));
napi_value global;
NODE_API_CALL(env, napi_get_global(env, &global));
bool equal;
NODE_API_CALL(env, napi_strict_equals(env, this_value, global, &equal));
printf("this == global = %s\n", equal ? "true" : "false");
// define a property with a normal value
napi_value property_value = Napi::String::New(env, "meow");
napi_set_named_property(env, this_value, "foo", property_value);
napi_value undefined;
NODE_API_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}
static napi_value getData_callback(napi_env env, napi_callback_info info) {
void *data;
NODE_API_CALL(env,
napi_get_cb_info(env, info, nullptr, nullptr, nullptr, &data));
const char *str_data = reinterpret_cast<const char *>(data);
napi_value ret;
NODE_API_CALL(env,
napi_create_string_utf8(env, str_data, NAPI_AUTO_LENGTH, &ret));
return ret;
}
static napi_value getStaticData_callback(napi_env env,
napi_callback_info info) {
void *data;
NODE_API_CALL(env,
napi_get_cb_info(env, info, nullptr, nullptr, nullptr, &data));
const char *str_data = reinterpret_cast<const char *>(data);
napi_value ret;
if (data) {
NODE_API_CALL(
env, napi_create_string_utf8(env, str_data, NAPI_AUTO_LENGTH, &ret));
} else {
// we should hit this case as the data pointer should be null
NODE_API_CALL(env, napi_get_undefined(env, &ret));
}
return ret;
}
static napi_value static_getter_callback(napi_env env,
napi_callback_info info) {
void *data;
NODE_API_CALL(env,
napi_get_cb_info(env, info, nullptr, nullptr, nullptr, &data));
const char *str_data = reinterpret_cast<const char *>(data);
napi_value ret;
if (data) {
NODE_API_CALL(
env, napi_create_string_utf8(env, str_data, NAPI_AUTO_LENGTH, &ret));
} else {
// we should hit this case as the data pointer should be null
NODE_API_CALL(env, napi_get_undefined(env, &ret));
}
return ret;
}
static napi_value get_class_with_constructor(const Napi::CallbackInfo &info) {
static char constructor_data[] = "constructor data";
static char method_data[] = "method data";
static char wrap_data[] = "wrap data";
napi_env env = info.Env();
napi_value napi_class;
const napi_property_descriptor property = {
.utf8name = "getData",
.name = nullptr,
.method = getData_callback,
.getter = nullptr,
.setter = nullptr,
.value = nullptr,
.attributes = napi_default_method,
.data = reinterpret_cast<void *>(method_data),
};
const napi_property_descriptor static_properties[] = {
{
.utf8name = "getStaticData",
.name = nullptr,
.method = getStaticData_callback,
.getter = nullptr,
.setter = nullptr,
.value = nullptr,
.attributes = napi_default_method,
// the class's data pointer should not be used instead -- it should
// stay nullptr
.data = nullptr,
},
{
.utf8name = "getter",
.name = nullptr,
.method = nullptr,
.getter = static_getter_callback,
.setter = nullptr,
.value = nullptr,
.attributes = napi_default,
// the class's data pointer should not be used instead -- it should
// stay nullptr
.data = nullptr,
},
};
NODE_API_CALL(
env, napi_define_class(env, "NapiClass", NAPI_AUTO_LENGTH, constructor,
reinterpret_cast<void *>(constructor_data), 1,
&property, &napi_class));
NODE_API_CALL(env,
napi_define_properties(env, napi_class, 2, static_properties));
NODE_API_CALL(env,
napi_wrap(env, napi_class, reinterpret_cast<void *>(wrap_data),
nullptr, nullptr, nullptr));
return napi_class;
}
void register_class_test(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, get_class_with_constructor);
}
} // namespace napitests

View File

@@ -0,0 +1,12 @@
#pragma once
// Functions exported to JS that make a class available with some interesting
// properties and methods
#include <napi.h>
namespace napitests {
void register_class_test(Napi::Env env, Napi::Object exports);
} // namespace napitests

View File

@@ -0,0 +1,173 @@
#include "conversion_tests.h"
#include "utils.h"
#include <array>
#include <cmath>
#include <utility>
namespace napitests {
// double_to_i32(any): number|undefined
static napi_value double_to_i32(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value input = info[0];
int32_t integer;
napi_value result;
napi_status status = napi_get_value_int32(env, input, &integer);
if (status == napi_ok) {
NODE_API_CALL(env, napi_create_int32(env, integer, &result));
} else {
NODE_API_ASSERT(env, status == napi_number_expected);
NODE_API_CALL(env, napi_get_undefined(env, &result));
}
return result;
}
// double_to_u32(any): number|undefined
static napi_value double_to_u32(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value input = info[0];
uint32_t integer;
napi_value result;
napi_status status = napi_get_value_uint32(env, input, &integer);
if (status == napi_ok) {
NODE_API_CALL(env, napi_create_uint32(env, integer, &result));
} else {
NODE_API_ASSERT(env, status == napi_number_expected);
NODE_API_CALL(env, napi_get_undefined(env, &result));
}
return result;
}
// double_to_i64(any): number|undefined
static napi_value double_to_i64(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value input = info[0];
int64_t integer;
napi_value result;
napi_status status = napi_get_value_int64(env, input, &integer);
if (status == napi_ok) {
NODE_API_CALL(env, napi_create_int64(env, integer, &result));
} else {
NODE_API_ASSERT(env, status == napi_number_expected);
NODE_API_CALL(env, napi_get_undefined(env, &result));
}
return result;
}
// test from the C++ side
static napi_value
test_number_integer_conversions(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
using f64_limits = std::numeric_limits<double>;
using i32_limits = std::numeric_limits<int32_t>;
using u32_limits = std::numeric_limits<uint32_t>;
using i64_limits = std::numeric_limits<int64_t>;
std::array<std::pair<double, int32_t>, 14> i32_cases{{
// special values
{f64_limits::infinity(), 0},
{-f64_limits::infinity(), 0},
{f64_limits::quiet_NaN(), 0},
// normal
{0.0, 0},
{1.0, 1},
{-1.0, -1},
// truncation
{1.25, 1},
{-1.25, -1},
// limits
{i32_limits::min(), i32_limits::min()},
{i32_limits::max(), i32_limits::max()},
// wrap around
{static_cast<double>(i32_limits::min()) - 1.0, i32_limits::max()},
{static_cast<double>(i32_limits::max()) + 1.0, i32_limits::min()},
{static_cast<double>(i32_limits::min()) - 2.0, i32_limits::max() - 1},
{static_cast<double>(i32_limits::max()) + 2.0, i32_limits::min() + 1},
}};
for (const auto &[in, expected_out] : i32_cases) {
napi_value js_in;
NODE_API_CALL(env, napi_create_double(env, in, &js_in));
int32_t out_from_napi;
NODE_API_CALL(env, napi_get_value_int32(env, js_in, &out_from_napi));
NODE_API_ASSERT(env, out_from_napi == expected_out);
}
std::array<std::pair<double, uint32_t>, 12> u32_cases{{
// special values
{f64_limits::infinity(), 0},
{-f64_limits::infinity(), 0},
{f64_limits::quiet_NaN(), 0},
// normal
{0.0, 0},
{1.0, 1},
// truncation
{1.25, 1},
{-1.25, u32_limits::max()},
// limits
{u32_limits::max(), u32_limits::max()},
// wrap around
{-1.0, u32_limits::max()},
{static_cast<double>(u32_limits::max()) + 1.0, 0},
{-2.0, u32_limits::max() - 1},
{static_cast<double>(u32_limits::max()) + 2.0, 1},
}};
for (const auto &[in, expected_out] : u32_cases) {
napi_value js_in;
NODE_API_CALL(env, napi_create_double(env, in, &js_in));
uint32_t out_from_napi;
NODE_API_CALL(env, napi_get_value_uint32(env, js_in, &out_from_napi));
NODE_API_ASSERT(env, out_from_napi == expected_out);
}
std::array<std::pair<double, int64_t>, 12> i64_cases{
{// special values
{f64_limits::infinity(), 0},
{-f64_limits::infinity(), 0},
{f64_limits::quiet_NaN(), 0},
// normal
{0.0, 0},
{1.0, 1},
{-1.0, -1},
// truncation
{1.25, 1},
{-1.25, -1},
// limits
// i64 max can't be precisely represented as double so it would round to
// 1 + i64 max, which would clamp and we don't want that yet. so we test
// the largest double smaller than i64 max instead (which is i64 max -
// 1024)
{i64_limits::min(), i64_limits::min()},
{std::nextafter(static_cast<double>(i64_limits::max()), 0.0),
static_cast<int64_t>(
std::nextafter(static_cast<double>(i64_limits::max()), 0.0))},
// clamp
{i64_limits::min() - 4096.0, i64_limits::min()},
{i64_limits::max() + 4096.0, i64_limits::max()}}};
for (const auto &[in, expected_out] : i64_cases) {
napi_value js_in;
NODE_API_CALL(env, napi_create_double(env, in, &js_in));
int64_t out_from_napi;
NODE_API_CALL(env, napi_get_value_int64(env, js_in, &out_from_napi));
NODE_API_ASSERT(env, out_from_napi == expected_out);
}
return ok(env);
}
void register_conversion_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, double_to_i32);
REGISTER_FUNCTION(env, exports, double_to_u32);
REGISTER_FUNCTION(env, exports, double_to_i64);
REGISTER_FUNCTION(env, exports, test_number_integer_conversions);
}
} // namespace napitests

View File

@@ -0,0 +1,12 @@
#pragma once
// Includes both some callbacks for module.js to use, and a long pure-C++ test
// of Node-API conversion functions
#include "napi_with_version.h"
namespace napitests {
void register_conversion_tests(Napi::Env env, Napi::Object exports);
} // namespace napitests

View File

@@ -0,0 +1,306 @@
#include "js_test_helpers.h"
#include "utils.h"
#include <map>
#include <string>
namespace napitests {
static bool finalize_called = false;
static void finalize_cb(napi_env env, void *finalize_data,
void *finalize_hint) {
node_api_post_finalizer(
env,
+[](napi_env env, void *data, void *hint) {
napi_handle_scope hs;
NODE_API_CALL_CUSTOM_RETURN(env, void(),
napi_open_handle_scope(env, &hs));
NODE_API_CALL_CUSTOM_RETURN(env, void(),
napi_close_handle_scope(env, hs));
finalize_called = true;
},
finalize_data, finalize_hint);
}
static napi_value create_ref_with_finalizer(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value object;
NODE_API_CALL(env, napi_create_object(env, &object));
napi_ref ref;
NODE_API_CALL(env,
napi_wrap(env, object, nullptr, finalize_cb, nullptr, &ref));
return ok(env);
}
static napi_value was_finalize_called(const Napi::CallbackInfo &info) {
napi_value ret;
NODE_API_CALL(info.Env(),
napi_get_boolean(info.Env(), finalize_called, &ret));
return ret;
}
// calls a function (the sole argument) which must throw. catches and returns
// the thrown error
static napi_value call_and_get_exception(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value fn = info[0];
napi_value undefined;
NODE_API_CALL(env, napi_get_undefined(env, &undefined));
NODE_API_ASSERT(env, napi_call_function(env, undefined, fn, 0, nullptr,
nullptr) == napi_pending_exception);
bool is_pending;
NODE_API_CALL(env, napi_is_exception_pending(env, &is_pending));
NODE_API_ASSERT(env, is_pending);
napi_value exception;
NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception));
napi_valuetype type = get_typeof(env, exception);
printf("typeof thrown exception = %s\n", napi_valuetype_to_string(type));
NODE_API_CALL(env, napi_is_exception_pending(env, &is_pending));
NODE_API_ASSERT(env, !is_pending);
return exception;
}
// throw_error(code: string|undefined, msg: string|undefined,
// error_kind: 'error'|'type_error'|'range_error'|'syntax_error')
// if code and msg are JS undefined then change them to nullptr
static napi_value throw_error(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
Napi::Value js_code = info[0];
Napi::Value js_msg = info[1];
std::string error_kind = info[2].As<Napi::String>().Utf8Value();
// these are optional
const char *code = nullptr;
std::string code_str;
const char *msg = nullptr;
std::string msg_str;
if (js_code.IsString()) {
code_str = js_code.As<Napi::String>().Utf8Value();
code = code_str.c_str();
}
if (js_msg.IsString()) {
msg_str = js_msg.As<Napi::String>().Utf8Value();
msg = msg_str.c_str();
}
using ThrowFunction =
napi_status (*)(napi_env, const char *code, const char *msg);
std::map<std::string, ThrowFunction> functions{
{"error", napi_throw_error},
{"type_error", napi_throw_type_error},
{"range_error", napi_throw_range_error},
{"syntax_error", node_api_throw_syntax_error}};
auto throw_function = functions[error_kind];
if (msg == nullptr) {
NODE_API_ASSERT(env, throw_function(env, code, msg) == napi_invalid_arg);
return ok(env);
} else {
NODE_API_ASSERT(env, throw_function(env, code, msg) == napi_ok);
return nullptr;
}
}
// create_and_throw_error(code: any, msg: any,
// error_kind: 'error'|'type_error'|'range_error'|'syntax_error')
// if code and msg are JS null then change them to nullptr
static napi_value create_and_throw_error(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value js_code = info[0];
napi_value js_msg = info[1];
std::string error_kind = info[2].As<Napi::String>();
if (get_typeof(env, js_code) == napi_null) {
js_code = nullptr;
}
if (get_typeof(env, js_msg) == napi_null) {
js_msg = nullptr;
}
using CreateErrorFunction = napi_status (*)(
napi_env, napi_value code, napi_value msg, napi_value *result);
std::map<std::string, CreateErrorFunction> functions{
{"error", napi_create_error},
{"type_error", napi_create_type_error},
{"range_error", napi_create_range_error},
{"syntax_error", node_api_create_syntax_error}};
auto create_error_function = functions[error_kind];
napi_value err;
napi_status create_status = create_error_function(env, js_code, js_msg, &err);
// cases that should fail:
// - js_msg is nullptr
// - js_msg is not a string
// - js_code is not nullptr and not a string
// also we need to make sure not to call get_typeof with nullptr, since it
// asserts that napi_typeof succeeded
if (!js_msg || get_typeof(env, js_msg) != napi_string ||
(js_code && get_typeof(env, js_code) != napi_string)) {
// bun and node may return different errors here depending on in what order
// the parameters are checked, but what's important is that there is an
// error
NODE_API_ASSERT(env, create_status == napi_string_expected ||
create_status == napi_invalid_arg);
return ok(env);
} else {
NODE_API_ASSERT(env, create_status == napi_ok);
NODE_API_CALL(env, napi_throw(env, err));
return nullptr;
}
}
// perform_get(object, key)
static napi_value perform_get(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value obj = info[0];
napi_value key = info[1];
napi_status status;
napi_value value;
// if key is a string, try napi_get_named_property
napi_valuetype type = get_typeof(env, key);
if (type == napi_string) {
char buf[1024];
NODE_API_CALL(env,
napi_get_value_string_utf8(env, key, buf, 1024, nullptr));
status = napi_get_named_property(env, obj, buf, &value);
if (status == napi_ok) {
NODE_API_ASSERT(env, value != nullptr);
printf("value type = %d\n", get_typeof(env, value));
} else {
NODE_API_ASSERT(env, status == napi_pending_exception);
return ok(env);
}
}
status = napi_get_property(env, obj, key, &value);
if (status == napi_ok) {
NODE_API_ASSERT(env, value != nullptr);
printf("value type = %d\n", get_typeof(env, value));
return value;
} else {
return ok(env);
}
}
// perform_set(object, key, value)
static napi_value perform_set(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value obj = info[0];
napi_value key = info[1];
napi_value value = info[2];
napi_status status;
// if key is a string, try napi_get_named_property
napi_valuetype type = get_typeof(env, key);
if (type == napi_string) {
char buf[1024];
NODE_API_CALL(env,
napi_get_value_string_utf8(env, key, buf, 1024, nullptr));
status = napi_set_named_property(env, obj, buf, value);
if (status != napi_ok) {
NODE_API_ASSERT(env, status == napi_pending_exception);
return ok(env);
}
}
status = napi_set_property(env, obj, key, value);
if (status != napi_ok) {
NODE_API_ASSERT(env, status == napi_pending_exception);
}
return ok(env);
}
static napi_value make_empty_array(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value js_size = info[0];
uint32_t size;
NODE_API_CALL(env, napi_get_value_uint32(env, js_size, &size));
napi_value array;
NODE_API_CALL(env, napi_create_array_with_length(env, size, &array));
return array;
}
// add_tag(object, lower, upper)
static napi_value add_tag(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
napi_value object = info[0];
uint32_t lower, upper;
NODE_API_CALL(env, napi_get_value_uint32(env, info[1], &lower));
NODE_API_CALL(env, napi_get_value_uint32(env, info[2], &upper));
napi_type_tag tag = {.lower = lower, .upper = upper};
NODE_API_CALL(env, napi_type_tag_object(env, object, &tag));
return env.Undefined();
}
// try_add_tag(object, lower, upper): bool
// true if success
static napi_value try_add_tag(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
napi_value object = info[0];
uint32_t lower, upper;
assert(napi_get_value_uint32(env, info[1], &lower) == napi_ok);
assert(napi_get_value_uint32(env, info[2], &upper) == napi_ok);
napi_type_tag tag = {.lower = lower, .upper = upper};
napi_status status = napi_type_tag_object(env, object, &tag);
bool pending;
assert(napi_is_exception_pending(env, &pending) == napi_ok);
if (pending) {
napi_value ignore_exception;
assert(napi_get_and_clear_last_exception(env, &ignore_exception) ==
napi_ok);
(void)ignore_exception;
}
return Napi::Boolean::New(env, status == napi_ok);
}
// check_tag(object, lower, upper): bool
static napi_value check_tag(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
napi_value object = info[0];
uint32_t lower, upper;
NODE_API_CALL(env, napi_get_value_uint32(env, info[1], &lower));
NODE_API_CALL(env, napi_get_value_uint32(env, info[2], &upper));
napi_type_tag tag = {.lower = lower, .upper = upper};
bool matches;
NODE_API_CALL(env, napi_check_object_type_tag(env, object, &tag, &matches));
return Napi::Boolean::New(env, matches);
}
void register_js_test_helpers(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, create_ref_with_finalizer);
REGISTER_FUNCTION(env, exports, was_finalize_called);
REGISTER_FUNCTION(env, exports, call_and_get_exception);
REGISTER_FUNCTION(env, exports, perform_get);
REGISTER_FUNCTION(env, exports, perform_set);
REGISTER_FUNCTION(env, exports, throw_error);
REGISTER_FUNCTION(env, exports, create_and_throw_error);
REGISTER_FUNCTION(env, exports, make_empty_array);
REGISTER_FUNCTION(env, exports, add_tag);
REGISTER_FUNCTION(env, exports, try_add_tag);
REGISTER_FUNCTION(env, exports, check_tag);
}
} // namespace napitests

View File

@@ -0,0 +1,13 @@
#pragma once
// Functions that are used by tests implemented in module.js, rather than
// directly used by napi.test.ts, but are not complex enough or do not cleanly
// fit into a category to go in a separate C++ file
#include "napi_with_version.h"
namespace napitests {
void register_js_test_helpers(Napi::Env env, Napi::Object exports);
} // namespace napitests

137
test/napi/napi-app/leak-fixture.js generated Normal file
View File

@@ -0,0 +1,137 @@
const nativeTests = require("./build/Debug/napitests.node");
function usage() {
return process.memoryUsage.rss();
}
function gc() {
if (typeof Bun == "object") {
Bun.gc(true);
} else {
global.gc();
}
}
async function test(fn, warmupRuns, testRuns, maxDeltaMB) {
gc();
// warmup
for (let i = 0; i < warmupRuns; i++) {
console.log(`warmup ${i}/${warmupRuns}`);
fn();
await new Promise(resolve => setTimeout(resolve, 0));
gc();
}
const initial = usage() / 1024 / 1024;
// test
for (let i = 0; i < testRuns; i++) {
console.log(`test ${i}/${testRuns}`);
fn();
await new Promise(resolve => setTimeout(resolve, 0));
gc();
}
const after = usage() / 1024 / 1024;
const deltaMB = after - initial;
console.log(`RSS ${initial} -> ${after} MiB`);
console.log(`Delta ${deltaMB} MB`);
if (deltaMB > maxDeltaMB) {
throw new Error("leaked!");
}
}
// Create a bunch of weak references and delete them
// Checks that napi_delete_reference cleans up memory associated with the napi_ref itself
function batchWeakRefs(n) {
if (typeof n != "number") throw new TypeError();
for (let i = 0; i < n; i++) {
// create tons of weak references to objects that get destroyed
nativeTests.add_weak_refs({});
}
// free all the weak refs
nativeTests.clear_weak_refs();
}
// Checks that strong references don't keep the value
function batchStrongRefs(n) {
if (typeof n != "number") throw new TypeError();
for (let i = 0; i < n; i++) {
const array = new Uint8Array(10_000_000);
array.fill(i);
nativeTests.create_and_delete_strong_ref(array);
}
}
function batchWrappedObjects(n) {
if (typeof n != "number") throw new TypeError();
let wraps = [];
for (let i = 0; i < n; i++) {
const s = Math.random().toString();
const wrapped = nativeTests.wrapped_object_factory(
s,
!process.isBun, // supports_node_api_post_finalize
);
wraps.push(wrapped);
if (wrapped.get() != s) {
throw new Error("wrong value");
}
}
gc();
for (const w of wraps) {
w.get();
}
// now GC them
}
function batchExternals(n) {
if (typeof n != "number") throw new TypeError();
let externals = [];
for (let i = 0; i < n; i++) {
const s = Math.random().toString();
const external = nativeTests.external_factory(s);
externals.push(external);
if (nativeTests.external_get(external) != s) {
throw new Error("wrong value");
}
}
gc();
for (const e of externals) {
nativeTests.external_get(e);
}
}
function batchThreadsafeFunctions(n, maxQueueSize) {
if (typeof n != "number") throw new TypeError();
const callback = () => {};
for (let i = 0; i < n; i++) {
nativeTests.create_and_delete_threadsafe_function(callback, maxQueueSize);
}
gc();
}
(async () => {
// TODO(@190n) get the rest of these tests working
// await test(() => batchWeakRefs(100), 10, 50, 8);
// await test(() => batchStrongRefs(100), 10, 50, 8);
// await test(() => batchWrappedObjects(1000), 20, 50, 20);
// await test(() => batchExternals(1000), 10, 400, 15);
// a queue size of 10,000 would leak 80 kB (each queue item is a void*), so 400 iterations
// would be a 32MB leak
// call with a preallocated queue
const threadsafeFunctionJsCallback = () => {};
await test(
() => nativeTests.create_and_delete_threadsafe_function(threadsafeFunctionJsCallback, 10_000, 10_000),
100,
400,
10,
);
// call with a dynamic queue
await test(
() => nativeTests.create_and_delete_threadsafe_function(threadsafeFunctionJsCallback, 0, 10_000),
100,
400,
10,
);
})();

View File

@@ -0,0 +1,193 @@
#include "leak_tests.h"
#include "utils.h"
#include <cassert>
#include <vector>
namespace napitests {
static std::vector<Napi::Reference<Napi::Value>> global_weak_refs;
// add a weak reference to a global array
// this will cause extra memory usage for the ref, but it should not retain the
// JS object being referenced
Napi::Value add_weak_refs(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
for (int i = 0; i < 50; i++) {
global_weak_refs.emplace_back(
Napi::Reference<Napi::Value>::New(info[0], 0));
}
return env.Undefined();
}
// delete all the weak refs created by add_weak_ref
Napi::Value clear_weak_refs(const Napi::CallbackInfo &info) {
global_weak_refs.clear();
return info.Env().Undefined();
}
// create a strong reference to a JS value, and then delete it
Napi::Value create_and_delete_strong_ref(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
// strong reference
auto ref = Napi::Reference<Napi::Value>::New(info[0], 2);
// destructor will be called
return env.Undefined();
}
class WrappedObject {
public:
static napi_value factory(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
napi_value s = info[0];
bool supports_node_api_post_finalize = info[1].As<Napi::Boolean>();
size_t len = 0;
NODE_API_CALL(env, napi_get_value_string_utf8(env, s, nullptr, 0, &len));
char *string = new char[len + 1];
string[len] = 0;
NODE_API_CALL(env,
napi_get_value_string_utf8(env, s, string, len + 1, nullptr));
napi_value js_object;
NODE_API_CALL(env, napi_create_object(env, &js_object));
WrappedObject *native_object =
new WrappedObject(string, supports_node_api_post_finalize);
NODE_API_CALL(env, napi_wrap(env, js_object, native_object, basic_finalize,
nullptr, &native_object->m_ref));
napi_property_descriptor property = {
.utf8name = "get",
.name = nullptr,
.method = get,
.getter = nullptr,
.setter = nullptr,
.value = nullptr,
.attributes = napi_default_method,
.data = nullptr,
};
NODE_API_CALL(env, napi_define_properties(env, js_object, 1, &property));
return js_object;
}
static napi_value get(napi_env env, napi_callback_info info) {
napi_value js_this;
NODE_API_CALL(
env, napi_get_cb_info(env, info, nullptr, nullptr, &js_this, nullptr));
WrappedObject *native_object;
NODE_API_CALL(env, napi_unwrap(env, js_this,
reinterpret_cast<void **>(&native_object)));
return Napi::String::New(env, native_object->m_string);
}
private:
static constexpr size_t big_alloc_size = 5'000'000;
WrappedObject(char *string, bool supports_node_api_post_finalize)
: m_string(string), m_big_alloc(new char[big_alloc_size]),
m_supports_node_api_post_finalize(supports_node_api_post_finalize) {
memset(m_big_alloc, big_alloc_size, 'x');
}
~WrappedObject() {
delete[] m_string;
delete[] m_big_alloc;
}
static void delete_ref(napi_env env, void *data, void *hint) {
napi_delete_reference(env, reinterpret_cast<napi_ref>(data));
}
static void basic_finalize(node_api_basic_env env, void *data, void *hint) {
auto *native_object = reinterpret_cast<WrappedObject *>(data);
if (native_object->m_supports_node_api_post_finalize) {
node_api_post_finalizer(env, delete_ref,
reinterpret_cast<void *>(native_object->m_ref),
nullptr);
} else {
napi_delete_reference(env, native_object->m_ref);
}
delete native_object;
}
char *m_string;
char *m_big_alloc;
napi_ref m_ref = nullptr;
bool m_supports_node_api_post_finalize;
};
class ExternalObject {
public:
static napi_value factory(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
std::string s = info[0].As<Napi::String>();
auto *native_object = new ExternalObject(std::move(s));
napi_value js_external;
NODE_API_CALL(env, napi_create_external(env, native_object, basic_finalize,
nullptr, &js_external));
return js_external;
}
static napi_value get(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
napi_value v = info[0];
ExternalObject *native_object;
NODE_API_CALL(env, napi_get_value_external(
env, v, reinterpret_cast<void **>(&native_object)));
return Napi::String::New(env, native_object->m_string);
}
private:
ExternalObject(std::string &&string) : m_string(string) {}
static void basic_finalize(node_api_basic_env env, void *data, void *hint) {
auto *native_object = reinterpret_cast<ExternalObject *>(data);
delete native_object;
}
std::string m_string;
};
// creates a threadsafe function wrapping the passed JavaScript function, and
// then deletes it
// parameter 1: JavaScript function
// parameter 2: max queue size (0 means dynamic, like in
// napi_create_threadsafe_function)
// parameter 3: number of times to call the threadsafe function
napi_value
create_and_delete_threadsafe_function(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
napi_value js_func = info[0];
size_t max_queue_size = info[1].As<Napi::Number>().Uint32Value();
size_t num_calls = info[2].As<Napi::Number>().Uint32Value();
NODE_API_ASSERT(env, num_calls <= max_queue_size || max_queue_size == 0);
napi_threadsafe_function tsfn;
napi_value async_resource_name;
NODE_API_CALL(env,
napi_create_string_utf8(env, "name", 4, &async_resource_name));
NODE_API_CALL(env,
napi_create_threadsafe_function(
env, js_func, nullptr, async_resource_name, max_queue_size,
1, nullptr, nullptr, nullptr, nullptr, &tsfn));
for (size_t i = 0; i < num_calls; i++) {
// status should never be napi_queue_full, because we call this exactly as
// many times as there is capacity in the queue
NODE_API_CALL(env, napi_call_threadsafe_function(tsfn, nullptr,
napi_tsfn_nonblocking));
}
NODE_API_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_abort));
return env.Undefined();
}
void register_leak_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, add_weak_refs);
REGISTER_FUNCTION(env, exports, clear_weak_refs);
REGISTER_FUNCTION(env, exports, create_and_delete_strong_ref);
REGISTER_FUNCTION(env, exports, create_and_delete_threadsafe_function);
exports.Set("wrapped_object_factory",
Napi::Function::New(env, WrappedObject::factory));
exports.Set("external_factory",
Napi::Function::New(env, ExternalObject::factory));
exports.Set("external_get", Napi::Function::New(env, ExternalObject::get));
}
} // namespace napitests

View File

@@ -0,0 +1,12 @@
#pragma once
// Helper functions used by JS to test that napi_ref, napi_wrap, and
// napi_external don't leak memory
#include "napi_with_version.h"
namespace napitests {
void register_leak_tests(Napi::Env env, Napi::Object exports);
} // namespace napitests

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,6 @@ try {
.catch(e => {
console.error("rejected:", e);
});
result.then(x => console.log("resolved to", x));
} else if (process.argv[2] == "eval_wrapper") {
// eval_wrapper just returns the result of the expression so it shouldn't be an error
console.log(result);

View File

@@ -1,4 +1,27 @@
const nativeTests = require("./build/Release/napitests.node");
const nativeTests = require("./build/Debug/napitests.node");
function assert(ok) {
if (!ok) throw new Error("assertion failed");
}
async function gcUntil(fn) {
const MAX = 100;
for (let i = 0; i < MAX; i++) {
await new Promise(resolve => {
setTimeout(resolve, 1);
});
if (typeof Bun == "object") {
Bun.gc(true);
} else {
// if this fails, you need to pass --expose-gc to node
global.gc();
}
if (fn()) {
return;
}
}
throw new Error(`Condition was not met after ${MAX} GC attempts`);
}
nativeTests.test_napi_class_constructor_handle_scope = () => {
const NapiClass = nativeTests.get_class_with_constructor();
@@ -12,16 +35,7 @@ nativeTests.test_napi_handle_scope_finalizer = async () => {
nativeTests.create_ref_with_finalizer(Boolean(process.isBun));
// Wait until it actually has been collected by ticking the event loop and forcing GC
while (!nativeTests.was_finalize_called()) {
await new Promise(resolve => {
setTimeout(() => resolve(), 0);
});
if (process.isBun) {
Bun.gc(true);
} else if (global.gc) {
global.gc();
}
}
await gcUntil(() => nativeTests.was_finalize_called());
};
nativeTests.test_promise_with_threadsafe_function = async () => {
@@ -54,6 +68,7 @@ nativeTests.test_get_property = () => {
},
},
{
// setter but no getter
set foo(newValue) {},
},
new Proxy(
@@ -66,7 +81,8 @@ nativeTests.test_get_property = () => {
),
5,
"hello",
// TODO(@190n) test null and undefined here on the napi fix branch
null,
undefined,
];
const keys = [
"foo",
@@ -90,7 +106,79 @@ nativeTests.test_get_property = () => {
const ret = nativeTests.perform_get(object, key);
console.log("native function returned", ret);
} catch (e) {
console.log("threw", e.toString());
if (e instanceof TypeError) {
const message = e.message;
assert(
message.includes("Cannot convert undefined or null to object") || message.includes("is not an object"),
);
} else {
console.log("threw", e.toString());
}
}
}
}
};
nativeTests.test_set_property = () => {
const objects = [
{},
{ foo: "bar" },
{
set foo(value) {
throw new Error(`set foo to ${value}`);
},
},
{
// getter but no setter
get foo() {},
},
new Proxy(
{},
{
set(_target, key, value) {
throw new Error(`proxy set ${key} to ${value}`);
},
},
),
5,
"hello",
null,
undefined,
];
const keys = [
"foo",
{
toString() {
throw new Error("toString");
},
},
{
[Symbol.toPrimitive]() {
throw new Error("Symbol.toPrimitive");
},
},
"toString",
"slice",
];
for (const object of objects) {
for (const key of keys) {
console.log(objects.indexOf(object) + ", " + keys.indexOf(key));
try {
const ret = nativeTests.perform_set(object, key, 42);
console.log("native function returned", ret);
if (object[key] != 42) {
throw new Error("setting property did not throw an error, but the property was not actually set");
}
} catch (e) {
if (e instanceof TypeError) {
const message = e.message;
assert(
message.includes("Cannot convert undefined or null to object") || message.includes("is not an object"),
);
} else {
console.log("threw", e.toString());
}
}
}
}
@@ -130,9 +218,7 @@ nativeTests.test_number_integer_conversions_from_js = () => {
for (const [input, expectedOutput] of i32Cases) {
const actualOutput = nativeTests.double_to_i32(input);
console.log(`${input} as i32 => ${actualOutput}`);
if (actualOutput !== expectedOutput) {
console.error("wrong");
}
assert(actualOutput === expectedOutput);
}
const u32Cases = [
@@ -161,9 +247,7 @@ nativeTests.test_number_integer_conversions_from_js = () => {
for (const [input, expectedOutput] of u32Cases) {
const actualOutput = nativeTests.double_to_u32(input);
console.log(`${input} as u32 => ${actualOutput}`);
if (actualOutput !== expectedOutput) {
console.error("wrong");
}
assert(actualOutput === expectedOutput);
}
const i64Cases = [
@@ -196,9 +280,7 @@ nativeTests.test_number_integer_conversions_from_js = () => {
console.log(
`${typeof input == "number" ? input.toFixed(2) : input} as i64 => ${typeof actualOutput == "number" ? actualOutput.toFixed(2) : actualOutput}`,
);
if (actualOutput !== expectedOutput) {
console.error("wrong");
}
assert(actualOutput === expectedOutput);
}
};
@@ -212,9 +294,9 @@ nativeTests.test_create_array_with_length = () => {
};
nativeTests.test_throw_functions_exhaustive = () => {
for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) {
for (const code of [undefined, "", "error code"]) {
for (const msg of [undefined, "", "error message"]) {
for (const code of [undefined, "", "error code"]) {
for (const msg of [undefined, "", "error message"]) {
for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) {
try {
nativeTests.throw_error(code, msg, errorKind);
console.log(`napi_throw_${errorKind}(${code ?? "nullptr"}, ${msg ?? "nullptr"}) did not throw`);
@@ -253,7 +335,6 @@ nativeTests.test_type_tag = () => {
const o2 = {};
nativeTests.add_tag(o1, 1, 2);
try {
// re-tag
nativeTests.add_tag(o1, 1, 2);
@@ -264,10 +345,146 @@ nativeTests.test_type_tag = () => {
console.log("tagging non-object succeeds: ", !nativeTests.try_add_tag(null, 0, 0));
nativeTests.add_tag(o2, 3, 4);
console.log("o1 matches o1:", nativeTests.check_tag(o1, 1, 2));
console.log("o1 matches o2:", nativeTests.check_tag(o1, 3, 4));
console.log("o2 matches o1:", nativeTests.check_tag(o2, 1, 2));
console.log("o2 matches o2:", nativeTests.check_tag(o2, 3, 4));
assert(nativeTests.check_tag(o1, 1, 2));
assert(!nativeTests.check_tag(o1, 1, 3));
assert(!nativeTests.check_tag(o1, 2, 2));
assert(nativeTests.check_tag(o2, 3, 4));
assert(!nativeTests.check_tag(o2, 3, 5));
assert(!nativeTests.check_tag(o2, 4, 4));
};
// parameters to create_wrap are: object, ask_for_ref, strong
const createWrapWithoutRef = o => nativeTests.create_wrap(o, false, false);
const createWrapWithWeakRef = o => nativeTests.create_wrap(o, true, false);
const createWrapWithStrongRef = o => nativeTests.create_wrap(o, true, true);
nativeTests.test_wrap_lifetime_without_ref = async () => {
let object = { foo: "bar" };
assert(createWrapWithoutRef(object) === object);
assert(nativeTests.get_wrap_data(object) === 42);
object = undefined;
await gcUntil(() => nativeTests.was_wrap_finalize_called());
};
nativeTests.test_wrap_lifetime_with_weak_ref = async () => {
// this looks the same as test_wrap_lifetime_without_ref because it is -- these cases should behave the same
let object = { foo: "bar" };
assert(createWrapWithWeakRef(object) === object);
assert(nativeTests.get_wrap_data(object) === 42);
object = undefined;
await gcUntil(() => nativeTests.was_wrap_finalize_called());
};
nativeTests.test_wrap_lifetime_with_strong_ref = async () => {
let object = { foo: "bar" };
assert(createWrapWithStrongRef(object) === object);
assert(nativeTests.get_wrap_data(object) === 42);
object = undefined;
// still referenced by native module so this should fail
try {
await gcUntil(() => nativeTests.was_wrap_finalize_called());
throw new Error("object was garbage collected while still referenced by native code");
} catch (e) {
if (!e.toString().includes("Condition was not met")) {
throw e;
}
}
// can still get the value using the ref
assert(nativeTests.get_wrap_data_from_ref() === 42);
// now we free it
nativeTests.unref_wrapped_value();
await gcUntil(() => nativeTests.was_wrap_finalize_called());
};
nativeTests.test_remove_wrap_lifetime_with_weak_ref = async () => {
let object = { foo: "bar" };
assert(createWrapWithWeakRef(object) === object);
assert(nativeTests.get_wrap_data(object) === 42);
nativeTests.remove_wrap(object);
assert(nativeTests.get_wrap_data(object) === undefined);
assert(nativeTests.get_wrap_data_from_ref() === undefined);
assert(nativeTests.get_object_from_ref() === object);
object = undefined;
// ref will stop working once the object is collected
await gcUntil(() => nativeTests.get_object_from_ref() === undefined);
// finalizer shouldn't have been called
assert(nativeTests.was_wrap_finalize_called() === false);
};
nativeTests.test_remove_wrap_lifetime_with_strong_ref = async () => {
let object = { foo: "bar" };
assert(createWrapWithStrongRef(object) === object);
assert(nativeTests.get_wrap_data(object) === 42);
nativeTests.remove_wrap(object);
assert(nativeTests.get_wrap_data(object) === undefined);
assert(nativeTests.get_wrap_data_from_ref() === undefined);
assert(nativeTests.get_object_from_ref() === object);
object = undefined;
// finalizer should not be called and object should not be freed
try {
await gcUntil(() => nativeTests.was_wrap_finalize_called() || nativeTests.get_object_from_ref() === undefined);
throw new Error("finalizer ran");
} catch (e) {
if (!e.toString().includes("Condition was not met")) {
throw e;
}
}
// native code can still get the object
assert(JSON.stringify(nativeTests.get_object_from_ref()) === `{"foo":"bar"}`);
// now it gets deleted
nativeTests.unref_wrapped_value();
await gcUntil(() => nativeTests.get_object_from_ref() === undefined);
};
nativeTests.test_napi_class = () => {
const NapiClass = nativeTests.get_class_with_constructor();
const instance = new NapiClass();
console.log("static data =", NapiClass.getStaticData());
console.log("static getter =", NapiClass.getter);
console.log("foo =", instance.foo);
console.log("data =", instance.getData());
};
nativeTests.test_subclass_napi_class = () => {
const NapiClass = nativeTests.get_class_with_constructor();
class Subclass extends NapiClass {}
const instance = new Subclass();
console.log("subclass static data =", Subclass.getStaticData());
console.log("subclass static getter =", Subclass.getter);
console.log("subclass foo =", instance.foo);
console.log("subclass data =", instance.getData());
};
nativeTests.test_napi_class_non_constructor_call = () => {
const NapiClass = nativeTests.get_class_with_constructor();
console.log("non-constructor call NapiClass() =", NapiClass());
console.log("global foo set to ", typeof foo != "undefined" ? foo : undefined);
};
nativeTests.test_reflect_construct_napi_class = () => {
const NapiClass = nativeTests.get_class_with_constructor();
let instance = Reflect.construct(NapiClass, [], Object);
console.log("reflect constructed foo =", instance.foo);
console.log("reflect constructed data =", instance.getData?.());
class Foo {}
instance = Reflect.construct(NapiClass, [], Foo);
console.log("reflect constructed foo =", instance.foo);
console.log("reflect constructed data =", instance.getData?.());
};
module.exports = nativeTests;

View File

@@ -0,0 +1,8 @@
#pragma once
#define NAPI_EXPERIMENTAL
#include <napi.h>
#include <node.h>
// TODO(@190n): remove this when CI has Node 22.6
typedef struct napi_env__ *napi_env;
typedef napi_env node_api_basic_env;

View File

@@ -0,0 +1,381 @@
#include "standalone_tests.h"
#include <algorithm>
#include <array>
#include <iostream>
#include <string>
#include "utils.h"
namespace napitests {
// https://github.com/oven-sh/bun/issues/7685
static napi_value test_issue_7685(const Napi::CallbackInfo &info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
// info[0] is a function to run the GC
NODE_API_ASSERT(env, info[1].IsNumber());
NODE_API_ASSERT(env, info[2].IsNumber());
NODE_API_ASSERT(env, info[3].IsNumber());
NODE_API_ASSERT(env, info[4].IsNumber());
NODE_API_ASSERT(env, info[5].IsNumber());
NODE_API_ASSERT(env, info[6].IsNumber());
NODE_API_ASSERT(env, info[7].IsNumber());
NODE_API_ASSERT(env, info[8].IsNumber());
return ok(env);
}
static napi_threadsafe_function tsfn_11949 = nullptr;
static void test_issue_11949_callback(napi_env env, napi_value js_callback,
void *opaque_context, void *opaque_data) {
int *context = reinterpret_cast<int *>(opaque_context);
int *data = reinterpret_cast<int *>(opaque_data);
printf("data = %d, context = %d\n", *data, *context);
delete context;
delete data;
napi_unref_threadsafe_function(env, tsfn_11949);
tsfn_11949 = nullptr;
}
// https://github.com/oven-sh/bun/issues/11949
static napi_value test_issue_11949(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
Napi::HandleScope scope(env);
napi_value name = Napi::String::New(env, "TSFN");
int *context = new int(42);
int *data = new int(1234);
NODE_API_CALL(env,
napi_create_threadsafe_function(
env, /* JavaScript function */ nullptr,
/* async resource */ nullptr, name,
/* max queue size (unlimited) */ 0,
/* initial thread count */ 1, /* finalize data */ nullptr,
/* finalize callback */ nullptr, context,
&test_issue_11949_callback, &tsfn_11949));
NODE_API_CALL(env, napi_call_threadsafe_function(tsfn_11949, data,
napi_tsfn_nonblocking));
return env.Undefined();
}
static void noop_callback(napi_env env, napi_value js_callback, void *context,
void *data) {}
static napi_value test_napi_threadsafe_function_does_not_hang_after_finalize(
const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
napi_value resource_name = Napi::String::New(env, "simple");
napi_threadsafe_function cb;
NODE_API_CALL(env,
napi_create_threadsafe_function(
env, /* JavaScript function */ nullptr,
/* async resource */ nullptr, resource_name,
/* max queue size (unlimited) */ 0,
/* initial thread count */ 1, /* finalize data */ nullptr,
/* finalize callback */ nullptr, /* context */ nullptr,
&noop_callback, &cb));
NODE_API_CALL(env, napi_release_threadsafe_function(cb, napi_tsfn_release));
printf("success!\n");
return env.Undefined();
}
static napi_value
test_napi_get_value_string_utf8_with_buffer(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
// info[0] is a function to run the GC
napi_value string_js = info[1];
// get how many chars we need to copy
size_t len = info[2].As<Napi::Number>().Uint32Value();
if (len == 424242) {
len = NAPI_AUTO_LENGTH;
} else {
NODE_API_ASSERT(env, len <= 29);
}
size_t copied;
const size_t BUF_SIZE = 30;
char buf[BUF_SIZE];
memset(buf, '*', BUF_SIZE);
buf[BUF_SIZE - 1] = '\0';
NODE_API_CALL(env,
napi_get_value_string_utf8(env, string_js, buf, len, &copied));
std::cout << "Chars to copy: " << len << std::endl;
std::cout << "Copied chars: " << copied << std::endl;
std::cout << "Buffer: ";
for (size_t i = 0; i < BUF_SIZE; i++) {
std::cout << (int)buf[i] << ", ";
}
std::cout << std::endl;
std::cout << "Value str: " << buf << std::endl;
return ok(env);
}
static napi_value
test_napi_handle_scope_string(const Napi::CallbackInfo &info) {
// this is mostly a copy of test_handle_scope_gc from
// test/v8/v8-module/main.cpp -- see comments there for explanation
Napi::Env env = info.Env();
constexpr size_t num_small_strings = 10000;
auto *small_strings = new napi_value[num_small_strings];
for (size_t i = 0; i < num_small_strings; i++) {
std::string cpp_str = std::to_string(i);
NODE_API_CALL(env,
napi_create_string_utf8(env, cpp_str.c_str(), cpp_str.size(),
&small_strings[i]));
}
run_gc(info);
for (size_t j = 0; j < num_small_strings; j++) {
char buf[16];
size_t result;
NODE_API_CALL(env, napi_get_value_string_utf8(env, small_strings[j], buf,
sizeof buf, &result));
NODE_API_ASSERT(env, atoi(buf) == (int)j);
}
delete[] small_strings;
return ok(env);
}
static napi_value
test_napi_handle_scope_bigint(const Napi::CallbackInfo &info) {
// this is mostly a copy of test_handle_scope_gc from
// test/v8/v8-module/main.cpp -- see comments there for explanation
Napi::Env env = info.Env();
constexpr size_t num_small_ints = 10000;
constexpr size_t small_int_size = 100;
auto *small_ints = new napi_value[num_small_ints];
for (size_t i = 0; i < num_small_ints; i++) {
std::array<uint64_t, small_int_size> words;
words.fill(i + 1);
NODE_API_CALL(env, napi_create_bigint_words(env, 0, small_int_size,
words.data(), &small_ints[i]));
}
run_gc(info);
for (size_t j = 0; j < num_small_ints; j++) {
std::array<uint64_t, small_int_size> words;
int sign;
size_t word_count = words.size();
NODE_API_CALL(env, napi_get_value_bigint_words(env, small_ints[j], &sign,
&word_count, words.data()));
printf("%d, %zu\n", sign, word_count);
NODE_API_ASSERT(env, sign == 0 && word_count == words.size());
NODE_API_ASSERT(env,
std::all_of(words.begin(), words.end(),
[j](const uint64_t &w) { return w == j + 1; }));
}
delete[] small_ints;
return ok(env);
}
static napi_value test_napi_delete_property(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
// info[0] is a function to run the GC
napi_value object = info[1];
napi_valuetype type = get_typeof(env, object);
NODE_API_ASSERT(env, type == napi_object);
napi_value key = Napi::String::New(env, "foo");
napi_value non_configurable_key = Napi::String::New(env, "bar");
napi_value val;
NODE_API_CALL(env, napi_create_int32(env, 42, &val));
bool delete_result;
NODE_API_CALL(env, napi_delete_property(env, object, non_configurable_key,
&delete_result));
NODE_API_ASSERT(env, delete_result == false);
NODE_API_CALL(env, napi_delete_property(env, object, key, &delete_result));
NODE_API_ASSERT(env, delete_result == true);
bool has_property;
NODE_API_CALL(env, napi_has_property(env, object, key, &has_property));
NODE_API_ASSERT(env, has_property == false);
return ok(env);
}
// Returns false if any napi function failed
static bool store_escaped_handle(napi_env env, napi_value *out,
const char *str) {
// Allocate these values on the heap so they cannot be seen by stack scanning
// after this function returns. An earlier version tried putting them on the
// stack and using volatile stores to set them to nullptr, but that wasn't
// effective when the NAPI module was built in release mode as extra copies of
// the pointers would still be left in uninitialized stack memory.
napi_escapable_handle_scope *ehs = new napi_escapable_handle_scope;
napi_value *s = new napi_value;
napi_value *escaped = new napi_value;
NODE_API_CALL_CUSTOM_RETURN(env, false,
napi_open_escapable_handle_scope(env, ehs));
NODE_API_CALL_CUSTOM_RETURN(
env, false, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, s));
NODE_API_CALL_CUSTOM_RETURN(env, false,
napi_escape_handle(env, *ehs, *s, escaped));
// can't call a second time
NODE_API_ASSERT_CUSTOM_RETURN(env, false,
napi_escape_handle(env, *ehs, *s, escaped) ==
napi_escape_called_twice);
NODE_API_CALL_CUSTOM_RETURN(env, false,
napi_close_escapable_handle_scope(env, *ehs));
*out = *escaped;
delete escaped;
delete s;
delete ehs;
return true;
}
static napi_value
test_napi_escapable_handle_scope(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
// allocate space for a napi_value on the heap
// use store_escaped_handle to put the value into it
// trigger GC
// the napi_value should still be valid even though it can't be found on the
// stack, because it escaped into the current handle scope
constexpr const char *str = "this is a long string meow meow meow";
napi_value *hidden = new napi_value;
NODE_API_ASSERT(env, store_escaped_handle(env, hidden, str));
run_gc(info);
char buf[64];
size_t len;
NODE_API_CALL(
env, napi_get_value_string_utf8(env, *hidden, buf, sizeof(buf), &len));
NODE_API_ASSERT(env, len == strlen(str));
NODE_API_ASSERT(env, strcmp(buf, str) == 0);
delete hidden;
return ok(env);
}
static napi_value
test_napi_handle_scope_nesting(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
constexpr const char *str = "this is a long string meow meow meow";
// Create an outer handle scope, hidden on the heap (the one created in
// NAPIFunction::call is still on the stack
napi_handle_scope *outer_hs = new napi_handle_scope;
NODE_API_CALL(env, napi_open_handle_scope(env, outer_hs));
// Make a handle in the outer scope, on the heap so stack scanning can't see
// it
napi_value *outer_scope_handle = new napi_value;
NODE_API_CALL(env, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH,
outer_scope_handle));
// Make a new handle scope on the heap so that the outer handle scope isn't
// active anymore
napi_handle_scope *inner_hs = new napi_handle_scope;
NODE_API_CALL(env, napi_open_handle_scope(env, inner_hs));
// Force GC
run_gc(info);
// Try to read our first handle. Did the outer handle scope get
// collected now that it's not on the global object? The inner handle scope
// should be keeping it alive even though it's not on the stack.
char buf[64];
size_t len;
NODE_API_CALL(env, napi_get_value_string_utf8(env, *outer_scope_handle, buf,
sizeof(buf), &len));
NODE_API_ASSERT(env, len == strlen(str));
NODE_API_ASSERT(env, strcmp(buf, str) == 0);
// Clean up
NODE_API_CALL(env, napi_close_handle_scope(env, *inner_hs));
delete inner_hs;
NODE_API_CALL(env, napi_close_handle_scope(env, *outer_hs));
delete outer_hs;
delete outer_scope_handle;
return ok(env);
}
// call this with a bunch (>10) of string arguments representing increasing
// decimal numbers. ensures that the runtime does not let these arguments be
// freed.
//
// test_napi_handle_scope_many_args(() => gc(), '1', '2', '3', ...)
static napi_value
test_napi_handle_scope_many_args(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
run_gc(info);
// now if bun is broken a bunch of our args are dead, because node-addon-api
// uses a heap array for >6 args
for (size_t i = 1; i < info.Length(); i++) {
Napi::String s = info[i].As<Napi::String>();
NODE_API_ASSERT(env, s.Utf8Value() == std::to_string(i));
}
return env.Undefined();
}
static napi_value test_napi_ref(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value object;
NODE_API_CALL(env, napi_create_object(env, &object));
napi_ref ref;
NODE_API_CALL(env, napi_create_reference(env, object, 0, &ref));
napi_value from_ref;
NODE_API_CALL(env, napi_get_reference_value(env, ref, &from_ref));
NODE_API_ASSERT(env, from_ref != nullptr);
napi_valuetype typeof_result = get_typeof(env, from_ref);
NODE_API_ASSERT(env, typeof_result == napi_object);
return ok(env);
}
static napi_value test_napi_run_script(const Napi::CallbackInfo &info) {
napi_value ret = nullptr;
// info[0] is the GC callback
(void)napi_run_script(info.Env(), info[1], &ret);
return ret;
}
void register_standalone_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, test_issue_7685);
REGISTER_FUNCTION(env, exports, test_issue_11949);
REGISTER_FUNCTION(env, exports, test_napi_get_value_string_utf8_with_buffer);
REGISTER_FUNCTION(env, exports,
test_napi_threadsafe_function_does_not_hang_after_finalize);
REGISTER_FUNCTION(env, exports, test_napi_handle_scope_string);
REGISTER_FUNCTION(env, exports, test_napi_handle_scope_bigint);
REGISTER_FUNCTION(env, exports, test_napi_delete_property);
REGISTER_FUNCTION(env, exports, test_napi_escapable_handle_scope);
REGISTER_FUNCTION(env, exports, test_napi_handle_scope_nesting);
REGISTER_FUNCTION(env, exports, test_napi_handle_scope_many_args);
REGISTER_FUNCTION(env, exports, test_napi_ref);
REGISTER_FUNCTION(env, exports, test_napi_run_script);
}
} // namespace napitests

View File

@@ -0,0 +1,11 @@
#pragma once
// Functions that are run as the entire test by napi.test.ts
#include "napi_with_version.h"
namespace napitests {
void register_standalone_tests(Napi::Env env, Napi::Object exports);
} // namespace napitests

View File

@@ -0,0 +1,89 @@
#pragma once
#include "napi_with_version.h"
#include <climits>
// e.g NODE_API_CALL(env, napi_create_int32(env, 5, &my_napi_integer))
#define NODE_API_CALL(env, call) NODE_API_CALL_CUSTOM_RETURN(env, NULL, call)
// Version of NODE_API_CALL for functions not returning napi_value
#define NODE_API_CALL_CUSTOM_RETURN(env, value_to_return_if_threw, call) \
NODE_API_ASSERT_CUSTOM_RETURN(env, value_to_return_if_threw, \
(call) == napi_ok)
// Throw an error in the given napi_env and return if expr is false
#define NODE_API_ASSERT(env, expr) \
NODE_API_ASSERT_CUSTOM_RETURN(env, NULL, expr)
#ifdef _MSC_VER
#define CURRENT_FUNCTION_NAME __FUNCSIG__
#else
#define CURRENT_FUNCTION_NAME __PRETTY_FUNCTION__
#endif
// Version of NODE_API_ASSERT for functions not returning napi_value
#define NODE_API_ASSERT_CUSTOM_RETURN(ENV, VALUE_TO_RETURN_IF_THREW, EXPR) \
do { \
if (!(EXPR)) { \
bool is_pending; \
napi_is_exception_pending((ENV), &is_pending); \
/* If an exception is already pending, don't rethrow it */ \
if (!is_pending) { \
char buf[4096] = {0}; \
snprintf(buf, sizeof(buf) - 1, "%s (%s:%d): Assertion failed: %s", \
CURRENT_FUNCTION_NAME, __FILE__, __LINE__, #EXPR); \
napi_throw_error((ENV), NULL, buf); \
} \
return (VALUE_TO_RETURN_IF_THREW); \
} \
} while (0)
#define REGISTER_FUNCTION(ENV, EXPORTS, FUNCTION) \
EXPORTS.Set(#FUNCTION, Napi::Function::New(ENV, FUNCTION))
static inline napi_value ok(napi_env env) {
napi_value result;
napi_get_undefined(env, &result);
return result;
}
// For functions that take a garbage collection callback as the first argument
// (functions not called directly by module.js), use this to trigger GC
static inline void run_gc(const Napi::CallbackInfo &info) {
info[0].As<Napi::Function>().Call(0, nullptr);
}
// calls napi_typeof and asserts it returns napi_ok
static inline napi_valuetype get_typeof(napi_env env, napi_value value) {
napi_valuetype result;
// return an invalid napi_valuetype if the call to napi_typeof fails
NODE_API_CALL_CUSTOM_RETURN(env, static_cast<napi_valuetype>(INT_MAX),
napi_typeof(env, value, &result));
return result;
}
static inline const char *napi_valuetype_to_string(napi_valuetype type) {
switch (type) {
case napi_undefined:
return "undefined";
case napi_null:
return "null";
case napi_boolean:
return "boolean";
case napi_number:
return "number";
case napi_string:
return "string";
case napi_symbol:
return "symbol";
case napi_object:
return "object";
case napi_function:
return "function";
case napi_external:
return "external";
case napi_bigint:
return "bigint";
default:
return "unknown";
}
}

View File

@@ -0,0 +1,167 @@
#include "wrap_tests.h"
#include "utils.h"
#include <cassert>
namespace napitests {
static napi_ref ref_to_wrapped_object = nullptr;
static bool wrap_finalize_called = false;
static void delete_the_ref(napi_env env, void *_data, void *_hint) {
printf("delete_the_ref\n");
// not using NODE_API_ASSERT as this runs in a finalizer where allocating an
// error might cause a harder-to-debug crash
assert(ref_to_wrapped_object);
napi_delete_reference(env, ref_to_wrapped_object);
ref_to_wrapped_object = nullptr;
}
static void finalize_for_create_wrap(napi_env env, void *opaque_data,
void *opaque_hint) {
int *data = reinterpret_cast<int *>(opaque_data);
int *hint = reinterpret_cast<int *>(opaque_hint);
printf("finalize_for_create_wrap, data = %d, hint = %d\n", *data, *hint);
delete data;
delete hint;
if (ref_to_wrapped_object) {
node_api_post_finalizer(env, delete_the_ref, nullptr, nullptr);
}
wrap_finalize_called = true;
}
// create_wrap(js_object: object, ask_for_ref: boolean, strong: boolean): object
static napi_value create_wrap(const Napi::CallbackInfo &info) {
wrap_finalize_called = false;
napi_env env = info.Env();
napi_value js_object = info[0];
napi_value js_ask_for_ref = info[1];
bool ask_for_ref;
NODE_API_CALL(env, napi_get_value_bool(env, js_ask_for_ref, &ask_for_ref));
napi_value js_strong = info[2];
bool strong;
NODE_API_CALL(env, napi_get_value_bool(env, js_strong, &strong));
// wrap it
int *wrap_data = new int(42);
int *wrap_hint = new int(123);
NODE_API_CALL(env, napi_wrap(env, js_object, wrap_data,
finalize_for_create_wrap, wrap_hint,
ask_for_ref ? &ref_to_wrapped_object : nullptr));
if (ask_for_ref && strong) {
uint32_t new_refcount;
NODE_API_CALL(
env, napi_reference_ref(env, ref_to_wrapped_object, &new_refcount));
NODE_API_ASSERT(env, new_refcount == 1);
}
if (!ask_for_ref) {
ref_to_wrapped_object = nullptr;
}
return js_object;
}
// get_wrap_data(js_object: object): number
static napi_value get_wrap_data(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value js_object = info[0];
void *wrapped_data;
napi_status status = napi_unwrap(env, js_object, &wrapped_data);
if (status != napi_ok) {
napi_value undefined;
NODE_API_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}
napi_value js_number;
NODE_API_CALL(env,
napi_create_int32(env, *reinterpret_cast<int *>(wrapped_data),
&js_number));
return js_number;
}
// get_object_from_ref(): object
static napi_value get_object_from_ref(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value wrapped_object;
NODE_API_CALL(env, napi_get_reference_value(env, ref_to_wrapped_object,
&wrapped_object));
return wrapped_object;
}
// get_wrap_data_from_ref(): number|undefined
static napi_value get_wrap_data_from_ref(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value wrapped_object;
NODE_API_CALL(env, napi_get_reference_value(env, ref_to_wrapped_object,
&wrapped_object));
void *wrapped_data;
napi_status status = napi_unwrap(env, wrapped_object, &wrapped_data);
if (status == napi_ok) {
napi_value js_number;
NODE_API_CALL(env,
napi_create_int32(env, *reinterpret_cast<int *>(wrapped_data),
&js_number));
return js_number;
} else if (status == napi_invalid_arg) {
// no longer wrapped
napi_value undefined;
NODE_API_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
} else {
NODE_API_ASSERT(env, false && "this should not be reached");
return nullptr;
}
}
// remove_wrap_data(js_object: object): undefined
static napi_value remove_wrap(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
napi_value js_object = info[0];
void *wrap_data;
NODE_API_CALL(env, napi_remove_wrap(env, js_object, &wrap_data));
napi_value undefined;
NODE_API_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}
// unref_wrapped_value(): undefined
static napi_value unref_wrapped_value(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
uint32_t new_refcount;
NODE_API_CALL(
env, napi_reference_unref(env, ref_to_wrapped_object, &new_refcount));
// should never have been set higher than 1
NODE_API_ASSERT(env, new_refcount == 0);
napi_value undefined;
NODE_API_CALL(env, napi_get_undefined(env, &undefined));
return undefined;
}
static napi_value was_wrap_finalize_called(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
return Napi::Boolean::New(env, wrap_finalize_called);
}
void register_wrap_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, create_wrap);
REGISTER_FUNCTION(env, exports, get_wrap_data);
REGISTER_FUNCTION(env, exports, get_object_from_ref);
REGISTER_FUNCTION(env, exports, get_wrap_data_from_ref);
REGISTER_FUNCTION(env, exports, remove_wrap);
REGISTER_FUNCTION(env, exports, unref_wrapped_value);
REGISTER_FUNCTION(env, exports, was_wrap_finalize_called);
}
} // namespace napitests

View File

@@ -0,0 +1,11 @@
#pragma once
// Helper functions used by JS to test napi_wrap
#include "napi_with_version.h"
namespace napitests {
void register_wrap_tests(Napi::Env env, Napi::Object exports);
} // namespace napitests

View File

@@ -7,7 +7,7 @@ describe("napi", () => {
beforeAll(() => {
// build gyp
const install = spawnSync({
cmd: [bunExe(), "install", "--verbose"],
cmd: [bunExe(), "install", "--ignore-scripts"],
cwd: join(__dirname, "napi-app"),
stderr: "inherit",
env: bunEnv,
@@ -15,6 +15,18 @@ describe("napi", () => {
stdin: "inherit",
});
if (!install.success) {
throw new Error("install dependencies failed");
}
const build = spawnSync({
cmd: [bunExe(), "x", "node-gyp", "rebuild", "--debug", "-j", "max"],
cwd: join(__dirname, "napi-app"),
stderr: "inherit",
env: bunEnv,
stdout: "inherit",
stdin: "inherit",
});
if (!build.success) {
throw new Error("build failed");
}
});
@@ -146,7 +158,7 @@ describe("napi", () => {
describe("issue_11949", () => {
it("napi_call_threadsafe_function should accept null", () => {
const result = checkSameOutput("test_issue_11949", []);
expect(result).toStartWith("data: nullptr");
expect(result).toStartWith("data = 1234, context = 42");
});
});
@@ -199,6 +211,9 @@ describe("napi", () => {
it("exists while calling a napi_async_complete_callback", () => {
checkSameOutput("create_promise", [false]);
});
it("keeps arguments moved off the stack alive", () => {
checkSameOutput("test_napi_handle_scope_many_args", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]);
});
});
describe("escapable_handle_scope", () => {
@@ -262,18 +277,18 @@ describe("napi", () => {
describe("napi_run_script", () => {
it("evaluates a basic expression", () => {
checkSameOutput("eval_wrapper", ["5 * (1 + 2)"]);
checkSameOutput("test_napi_run_script", ["5 * (1 + 2)"]);
});
it("provides the right this value", () => {
checkSameOutput("eval_wrapper", ["this === global"]);
checkSameOutput("test_napi_run_script", ["this === global"]);
});
it("propagates exceptions", () => {
checkSameOutput("eval_wrapper", ["(()=>{ throw new TypeError('oops'); })()"]);
checkSameOutput("test_napi_run_script", ["(()=>{ throw new TypeError('oops'); })()"]);
});
it("cannot see locals from around its invocation", () => {
// variable should_not_exist is declared on main.js:18, but it should not be in scope for the eval'd code
// this doesn't use checkSameOutput because V8 and JSC use different error messages for a missing variable
let bunResult = runOn(bunExe(), "eval_wrapper", ["shouldNotExist"]);
let bunResult = runOn(bunExe(), "test_napi_run_script", ["shouldNotExist"]);
// remove all debug logs
bunResult = bunResult.replaceAll(/^\[\w+\].+$/gm, "").trim();
expect(bunResult).toBe(
@@ -288,6 +303,12 @@ describe("napi", () => {
});
});
describe("napi_set_named_property", () => {
it("handles edge cases", () => {
checkSameOutput("test_set_property", []);
});
});
describe("napi_value <=> integer conversion", () => {
it("works", () => {
checkSameOutput("test_number_integer_conversions_from_js", []);
@@ -319,6 +340,27 @@ describe("napi", () => {
checkSameOutput("test_type_tag", []);
});
});
// TODO(@190n) test allocating in a finalizer
describe("napi_wrap", () => {
it("cleans up objects at the right time", () => {
checkSameOutput("test_wrap_lifetime_without_ref", []);
checkSameOutput("test_wrap_lifetime_with_weak_ref", []);
checkSameOutput("test_wrap_lifetime_with_strong_ref", []);
checkSameOutput("test_remove_wrap_lifetime_with_weak_ref", []);
checkSameOutput("test_remove_wrap_lifetime_with_strong_ref", []);
});
});
describe("napi_define_class", () => {
it("handles edge cases in the constructor", () => {
checkSameOutput("test_napi_class", []);
checkSameOutput("test_subclass_napi_class", []);
checkSameOutput("test_napi_class_non_constructor_call", []);
checkSameOutput("test_reflect_construct_napi_class", []);
});
});
});
function checkSameOutput(test: string, args: any[] | string) {

1
test/napi/node-napi-tests/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
__pycache__

View File

@@ -0,0 +1,5 @@
These files are copied from https://github.com/190n/node/tree/napi-tests-bun, which in turn is a fork of Node.js with their js-native-api tests modified slightly to work in Bun.
To change these files, edit the Node.js fork and then copy the changed version here.
We should periodically (and definitely when we add new Node-API functions) sync that fork with Node.js upstream.

View File

@@ -0,0 +1,329 @@
// Copyright 2010 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
var except = "exception";
var correct_answer_index = 0;
var correct_answers = [
false, false, true, true, false, false, true, true,
true, false, false, true, true, false, false, true,
false, true, true, false, false, true, true, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, false, true,
except, except, true, false, except, except, true, false,
except, except, false, false, except, except, false, false,
false, false, except, except, false, false, except, except,
true, false, except, except, true, false, except, except,
false, true, except, except, false, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, false, false, true, true,
false, true, true, false, false, true, true, false,
true, true, false, false, false, true, true, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, true, false,
except, except, false, false, except, except, true, false,
false, false, except, except, false, false, except, except,
true, false, except, except, true, false, except, except,
false, true, except, except, false, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, true, true, false,
true, false, false, true, true, true, false, false,
false, true, true, false, false, true, true, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, false, true,
except, except, true, false, except, except, true, false,
except, except, false, false, except, except, false, false,
false, false, except, except, false, true, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, false, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, true, true, false,
true, false, false, true, false, true, true, false,
false, true, true, false, false, true, true, false,
true, true, false, false, false, true, true, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, true, false,
except, except, false, false, except, except, true, false,
false, false, except, except, false, true, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, false, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, false, false, true, true,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, false, false, except, except,
true, false, except, except, false, false, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, false, false, true, true,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, false, false, except, except,
true, false, except, except, false, false, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, true, true, false, false,
true, false, false, true, true, true, false, false,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, true, true, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, true, true, false, false,
true, false, false, true, true, true, false, false,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, true, true, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, true, true, false, false,
false, true, true, false, false, false, true, true,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, false, false,
except, except, true, false, except, except, true, true,
except, except, false, false, except, except, false, false,
false, false, except, except, false, false, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, false, false, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, false, false, true, true,
false, true, true, false, false, false, true, true,
true, true, false, false, false, false, true, true,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, true, true,
except, except, false, false, except, except, true, true,
false, false, except, except, false, false, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, false, false, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, true, true, false, false,
false, true, true, false, false, false, true, true,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, false, false,
except, except, true, false, except, except, true, true,
except, except, false, false, except, except, false, false,
false, false, except, except, false, false, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, false, false, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, false, false, true, true,
false, true, true, false, false, false, true, true,
true, true, false, false, false, false, true, true,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, true, true,
except, except, false, false, except, except, true, true,
false, false, except, except, false, false, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, false, false, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, false, false, true, true,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, false, false, except, except,
true, false, except, except, false, false, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, false, false, true, true,
true, false, false, true, false, false, true, true,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, false, false, except, except,
true, false, except, except, false, false, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, true, true, false, false,
true, false, false, true, true, true, false, false,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, true, true, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
false, false, true, true, true, true, false, false,
true, false, false, true, true, true, false, false,
false, true, true, false, true, true, false, false,
true, true, false, false, true, true, false, false,
except, except, true, true, except, except, true, true,
except, except, false, true, except, except, true, true,
except, except, true, false, except, except, false, false,
except, except, false, false, except, except, false, false,
false, false, except, except, true, true, except, except,
true, false, except, except, true, true, except, except,
false, true, except, except, true, true, except, except,
true, true, except, except, true, true, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except,
except, except, except, except, except, except, except, except];
for (var i = 0; i < 256; i++) {
Test(i & 1, i & 2, i & 4, i & 8, i & 0x10, i & 0x20, i & 0x40, i & 0x80);
}
function InstanceTest(x, func) {
try {
var answer = (x instanceof func);
assertEquals(correct_answers[correct_answer_index], answer);
} catch (e) {
assertTrue(/prototype/.test(e));
assertEquals(correct_answers[correct_answer_index], except);
}
correct_answer_index++;
}
function Test(a, b, c, d, e, f, g, h) {
var Foo = function() { }
var Bar = function() { }
if (c) Foo.prototype = 12;
if (d) Bar.prototype = 13;
var x = a ? new Foo() : new Bar();
var y = b ? new Foo() : new Bar();
InstanceTest(x, Foo);
InstanceTest(y, Foo);
InstanceTest(x, Bar);
InstanceTest(y, Bar);
if (e) x.__proto__ = Bar.prototype;
if (f) y.__proto__ = Foo.prototype;
if (g) {
x.__proto__ = y;
} else {
if (h) y.__proto__ = x
}
InstanceTest(x, Foo);
InstanceTest(y, Foo);
InstanceTest(x, Bar);
InstanceTest(y, Bar);
}

View File

@@ -0,0 +1,93 @@
// Copyright 2008 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
assertTrue({} instanceof Object);
assertTrue([] instanceof Object);
assertFalse({} instanceof Array);
assertTrue([] instanceof Array);
function TestChains() {
var A = {};
var B = {};
var C = {};
B.__proto__ = A;
C.__proto__ = B;
function F() { }
F.prototype = A;
assertTrue(C instanceof F);
assertTrue(B instanceof F);
assertFalse(A instanceof F);
F.prototype = B;
assertTrue(C instanceof F);
assertFalse(B instanceof F);
assertFalse(A instanceof F);
F.prototype = C;
assertFalse(C instanceof F);
assertFalse(B instanceof F);
assertFalse(A instanceof F);
}
TestChains();
function TestExceptions() {
function F() { }
var items = [ 1, new Number(42),
true,
'string', new String('hest'),
{}, [],
F, new F(),
Object, String ];
var exceptions = 0;
var instanceofs = 0;
for (var i = 0; i < items.length; i++) {
for (var j = 0; j < items.length; j++) {
try {
if (items[i] instanceof items[j]) instanceofs++;
} catch (e) {
assertTrue(e instanceof TypeError);
exceptions++;
}
}
}
assertEquals(10, instanceofs);
assertEquals(88, exceptions);
// Make sure to throw an exception if the function prototype
// isn't a proper JavaScript object.
function G() { }
G.prototype = undefined;
assertThrows("({} instanceof G)");
}
TestExceptions();

View File

@@ -0,0 +1,55 @@
# Node.js Core Tests
This directory contains code and data used to test the Node.js implementation.
For a detailed guide on how to write tests in this
directory, see [the guide on writing tests](../doc/contributing/writing-tests.md).
On how to run tests in this directory, see
[the contributing guide](../doc/contributing/pull-requests.md#step-6-test).
For the tests to run on Windows, be sure to clone Node.js source code with the
`autocrlf` git config flag set to true.
## Test Directories
| Directory | Runs on CI | Purpose |
| ---------------- | ---------- | ------------------------------------------------------------------------------------------------------------- |
| `abort` | Yes | Tests that use `--abort-on-uncaught-exception` and other cases where we want to avoid generating a core file. |
| `addons` | Yes | Tests for [addon][] functionality along with some tests that require an addon. |
| `async-hooks` | Yes | Tests for [async\_hooks][async_hooks] functionality. |
| `benchmark` | Yes | Test minimal functionality of benchmarks. |
| `cctest` | Yes | C++ tests that are run as part of the build process. |
| `code-cache` | No | Tests for a Node.js binary compiled with V8 code cache. |
| `common` | _N/A_ | Common modules shared among many tests.[^1] |
| `doctool` | Yes | Tests for the documentation generator. |
| `es-module` | Yes | Test ESM module loading. |
| `fixtures` | _N/A_ | Test fixtures used in various tests throughout the test suite. |
| `internet` | No | Tests that make real outbound network connections.[^2] |
| `js-native-api` | Yes | Tests for Node.js-agnostic [Node-API][] functionality. |
| `known_issues` | Yes | Tests reproducing known issues within the system.[^3] |
| `message` | Yes | Tests for messages that are output for various conditions |
| `node-api` | Yes | Tests for Node.js-specific [Node-API][] functionality. |
| `parallel` | Yes | Various tests that are able to be run in parallel. |
| `pseudo-tty` | Yes | Tests that require stdin/stdout/stderr to be a TTY. |
| `pummel` | No | Various tests for various modules / system functionality operating under load. |
| `sequential` | Yes | Various tests that must not run in parallel. |
| `testpy` | _N/A_ | Test configuration utility used by various test suites. |
| `tick-processor` | No | Tests for the V8 tick processor integration.[^4] |
| `v8-updates` | No | Tests for V8 performance integration. |
[^1]: [Documentation](../test/common/README.md)
[^2]: Tests for networking related modules may also be present in other directories, but those tests do
not make outbound connections.
[^3]: All tests inside of this directory are expected to fail. If a test doesn't fail on certain platforms,
those should be skipped via `known_issues.status`.
[^4]: The tests are for the logic in `lib/internal/v8_prof_processor.js` and `lib/internal/v8_prof_polyfill.js`.
The tests confirm that the profile processor packages the correct set of scripts from V8 and introduces the
correct platform specific logic.
[Node-API]: https://nodejs.org/api/n-api.html
[addon]: https://nodejs.org/api/addons.html
[async_hooks]: https://nodejs.org/api/async_hooks.html

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
'use strict';
const { Stream } = require('stream');
function noop() {}
// A stream to push an array into a REPL
function ArrayStream() {
this.run = function(data) {
data.forEach((line) => {
this.emit('data', `${line}\n`);
});
};
}
Object.setPrototypeOf(ArrayStream.prototype, Stream.prototype);
Object.setPrototypeOf(ArrayStream, Stream);
ArrayStream.prototype.readable = true;
ArrayStream.prototype.writable = true;
ArrayStream.prototype.pause = noop;
ArrayStream.prototype.resume = noop;
ArrayStream.prototype.write = noop;
module.exports = ArrayStream;

View File

@@ -0,0 +1,104 @@
'use strict';
const common = require('.');
const path = require('node:path');
const test = require('node:test');
const fs = require('node:fs/promises');
const assert = require('node:assert/strict');
const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g;
const windowNewlineRegexp = /\r/g;
function replaceNodeVersion(str) {
return str.replaceAll(process.version, '*');
}
function replaceStackTrace(str, replacement = '$1*$7$8\n') {
return str.replace(stackFramesRegexp, replacement);
}
function replaceWindowsLineEndings(str) {
return str.replace(windowNewlineRegexp, '');
}
function replaceWindowsPaths(str) {
return common.isWindows ? str.replaceAll(path.win32.sep, path.posix.sep) : str;
}
function replaceFullPaths(str) {
return str.replaceAll('\\\'', "'").replaceAll(path.resolve(__dirname, '../..'), '');
}
function transform(...args) {
return (str) => args.reduce((acc, fn) => fn(acc), str);
}
function getSnapshotPath(filename) {
const { name, dir } = path.parse(filename);
return path.resolve(dir, `${name}.snapshot`);
}
async function assertSnapshot(actual, filename = process.argv[1]) {
const snapshot = getSnapshotPath(filename);
if (process.env.NODE_REGENERATE_SNAPSHOTS) {
await fs.writeFile(snapshot, actual);
} else {
let expected;
try {
expected = await fs.readFile(snapshot, 'utf8');
} catch (e) {
if (e.code === 'ENOENT') {
console.log(
'Snapshot file does not exist. You can create a new one by running the test with NODE_REGENERATE_SNAPSHOTS=1',
);
}
throw e;
}
assert.strictEqual(actual, replaceWindowsLineEndings(expected));
}
}
/**
* Spawn a process and assert its output against a snapshot.
* if you want to automatically update the snapshot, run tests with NODE_REGENERATE_SNAPSHOTS=1
* transform is a function that takes the output and returns a string that will be compared against the snapshot
* this is useful for normalizing output such as stack traces
* there are some predefined transforms in this file such as replaceStackTrace and replaceWindowsLineEndings
* both of which can be used as an example for writing your own
* compose multiple transforms by passing them as arguments to the transform function:
* assertSnapshot.transform(assertSnapshot.replaceStackTrace, assertSnapshot.replaceWindowsLineEndings)
* @param {string} filename
* @param {function(string): string} [transform]
* @param {object} [options] - control how the child process is spawned
* @param {boolean} [options.tty] - whether to spawn the process in a pseudo-tty
* @returns {Promise<void>}
*/
async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...options } = {}) {
if (tty && common.isWindows) {
test({ skip: 'Skipping pseudo-tty tests, as pseudo terminals are not available on Windows.' });
return;
}
let flags = common.parseTestFlags(filename);
if (options.flags) {
flags = [...options.flags, ...flags];
}
const executable = tty ? (process.env.PYTHON || 'python3') : process.execPath;
const args =
tty ?
[path.join(__dirname, '../..', 'tools/pseudo-tty.py'), process.execPath, ...flags, filename] :
[...flags, filename];
const { stdout, stderr } = await common.spawnPromisified(executable, args, options);
await assertSnapshot(transform(`${stdout}${stderr}`), filename);
}
module.exports = {
assertSnapshot,
getSnapshotPath,
replaceFullPaths,
replaceNodeVersion,
replaceStackTrace,
replaceWindowsLineEndings,
replaceWindowsPaths,
spawnAndAssert,
transform,
};

View File

@@ -0,0 +1,54 @@
'use strict';
const assert = require('assert');
const fork = require('child_process').fork;
const path = require('path');
const runjs = path.join(__dirname, '..', '..', 'benchmark', 'run.js');
function runBenchmark(name, env) {
const argv = ['test'];
argv.push(name);
const mergedEnv = { ...process.env, ...env };
const child = fork(runjs, argv, {
env: mergedEnv,
stdio: ['inherit', 'pipe', 'inherit', 'ipc'],
});
child.stdout.setEncoding('utf8');
let stdout = '';
child.stdout.on('data', (line) => {
stdout += line;
});
child.on('exit', (code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
// This bit makes sure that each benchmark file is being sent settings such
// that the benchmark file runs just one set of options. This helps keep the
// benchmark tests from taking a long time to run. Therefore, stdout should be composed as follows:
// The first and last lines should be empty.
// Each test should be separated by a blank line.
// The first line of each test should contain the test's name.
// The second line of each test should contain the configuration for the test.
// If the test configuration is not a group, there should be exactly two lines.
// Otherwise, it is possible to have more than two lines.
const splitTests = stdout.split(/\n\s*\n/);
for (let testIdx = 1; testIdx < splitTests.length - 1; testIdx++) {
const lines = splitTests[testIdx].split('\n');
assert.ok(/.+/.test(lines[0]));
if (!lines[1].includes('group="')) {
assert.strictEqual(lines.length, 2, `benchmark file not running exactly one configuration in test: ${stdout}`);
}
}
});
}
module.exports = runBenchmark;

View File

@@ -0,0 +1,156 @@
'use strict';
const assert = require('assert');
const { spawnSync, execFileSync } = require('child_process');
const common = require('./');
const util = require('util');
// Workaround for Windows Server 2008R2
// When CMD is used to launch a process and CMD is killed too quickly, the
// process can stay behind running in suspended state, never completing.
function cleanupStaleProcess(filename) {
if (!common.isWindows) {
return;
}
process.once('beforeExit', () => {
const basename = filename.replace(/.*[/\\]/g, '');
try {
execFileSync(`${process.env.SystemRoot}\\System32\\wbem\\WMIC.exe`, [
'process',
'where',
`commandline like '%${basename}%child'`,
'delete',
'/nointeractive',
]);
} catch {
// Ignore failures, there might not be any stale process to clean up.
}
});
}
// This should keep the child process running long enough to expire
// the timeout.
const kExpiringChildRunTime = common.platformTimeout(20 * 1000);
const kExpiringParentTimer = 1;
assert(kExpiringChildRunTime > kExpiringParentTimer);
function logAfterTime(time) {
setTimeout(() => {
// The following console statements are part of the test.
console.log('child stdout');
console.error('child stderr');
}, time);
}
function checkOutput(str, check) {
if ((check instanceof RegExp && !check.test(str)) ||
(typeof check === 'string' && check !== str)) {
return { passed: false, reason: `did not match ${util.inspect(check)}` };
}
if (typeof check === 'function') {
try {
check(str);
} catch (error) {
return {
passed: false,
reason: `did not match expectation, checker throws:\n${util.inspect(error)}`,
};
}
}
return { passed: true };
}
function expectSyncExit(caller, spawnArgs, {
status,
signal,
stderr: stderrCheck,
stdout: stdoutCheck,
trim = false,
}) {
const child = spawnSync(...spawnArgs);
const failures = [];
let stderrStr, stdoutStr;
if (status !== undefined && child.status !== status) {
failures.push(`- process terminated with status ${child.status}, expected ${status}`);
}
if (signal !== undefined && child.signal !== signal) {
failures.push(`- process terminated with signal ${child.signal}, expected ${signal}`);
}
function logAndThrow() {
const tag = `[process ${child.pid}]:`;
console.error(`${tag} --- stderr ---`);
console.error(stderrStr === undefined ? child.stderr.toString() : stderrStr);
console.error(`${tag} --- stdout ---`);
console.error(stdoutStr === undefined ? child.stdout.toString() : stdoutStr);
console.error(`${tag} status = ${child.status}, signal = ${child.signal}`);
const error = new Error(`${failures.join('\n')}`);
if (spawnArgs[2]) {
error.options = spawnArgs[2];
}
let command = spawnArgs[0];
if (Array.isArray(spawnArgs[1])) {
command += ' ' + spawnArgs[1].join(' ');
}
error.command = command;
Error.captureStackTrace(error, caller);
throw error;
}
// If status and signal are not matching expectations, fail early.
if (failures.length !== 0) {
logAndThrow();
}
if (stderrCheck !== undefined) {
stderrStr = child.stderr.toString();
const { passed, reason } = checkOutput(trim ? stderrStr.trim() : stderrStr, stderrCheck);
if (!passed) {
failures.push(`- stderr ${reason}`);
}
}
if (stdoutCheck !== undefined) {
stdoutStr = child.stdout.toString();
const { passed, reason } = checkOutput(trim ? stdoutStr.trim() : stdoutStr, stdoutCheck);
if (!passed) {
failures.push(`- stdout ${reason}`);
}
}
if (failures.length !== 0) {
logAndThrow();
}
return { child, stderr: stderrStr, stdout: stdoutStr };
}
function spawnSyncAndExit(...args) {
const spawnArgs = args.slice(0, args.length - 1);
const expectations = args[args.length - 1];
return expectSyncExit(spawnSyncAndExit, spawnArgs, expectations);
}
function spawnSyncAndExitWithoutError(...args) {
return expectSyncExit(spawnSyncAndExitWithoutError, [...args], {
status: 0,
signal: null,
});
}
function spawnSyncAndAssert(...args) {
const expectations = args.pop();
return expectSyncExit(spawnSyncAndAssert, [...args], {
status: 0,
signal: null,
...expectations,
});
}
module.exports = {
cleanupStaleProcess,
logAfterTime,
kExpiringChildRunTime,
kExpiringParentTimer,
spawnSyncAndAssert,
spawnSyncAndExit,
spawnSyncAndExitWithoutError,
};

View File

@@ -0,0 +1,28 @@
'use strict';
const assert = require('assert');
const kLimit = Symbol('limit');
const kCallback = Symbol('callback');
const common = require('./');
class Countdown {
constructor(limit, cb) {
assert.strictEqual(typeof limit, 'number');
assert.strictEqual(typeof cb, 'function');
this[kLimit] = limit;
this[kCallback] = common.mustCall(cb);
}
dec() {
assert(this[kLimit] > 0, 'Countdown expired');
if (--this[kLimit] === 0)
this[kCallback]();
return this[kLimit];
}
get remaining() {
return this[kLimit];
}
}
module.exports = Countdown;

View File

@@ -0,0 +1,50 @@
'use strict';
require('./');
const fs = require('fs');
const path = require('path');
const assert = require('assert');
function getCpuProfiles(dir) {
const list = fs.readdirSync(dir);
return list
.filter((file) => file.endsWith('.cpuprofile'))
.map((file) => path.join(dir, file));
}
function getFrames(file, suffix) {
const data = fs.readFileSync(file, 'utf8');
const profile = JSON.parse(data);
const frames = profile.nodes.filter((i) => {
const frame = i.callFrame;
return frame.url.endsWith(suffix);
});
return { frames, nodes: profile.nodes };
}
function verifyFrames(output, file, suffix) {
const { frames, nodes } = getFrames(file, suffix);
if (frames.length === 0) {
// Show native debug output and the profile for debugging.
console.log(output.stderr.toString());
console.log(nodes);
}
assert.notDeepStrictEqual(frames, []);
}
// We need to set --cpu-interval to a smaller value to make sure we can
// find our workload in the samples. 50us should be a small enough sampling
// interval for this.
const kCpuProfInterval = 50;
const env = {
...process.env,
NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER',
};
module.exports = {
getCpuProfiles,
kCpuProfInterval,
env,
getFrames,
verifyFrames,
};

View File

@@ -0,0 +1,114 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const crypto = require('crypto');
const {
createSign,
createVerify,
publicEncrypt,
privateDecrypt,
sign,
verify,
} = crypto;
// The values below (modp2/modp2buf) are for a 1024 bits long prime from
// RFC 2412 E.2, see https://tools.ietf.org/html/rfc2412. */
const modp2buf = Buffer.from([
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f,
0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b,
0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22,
0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, 0xef, 0x95,
0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d,
0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51,
0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6,
0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff,
0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, 0xee, 0x38, 0x6b, 0xfb,
0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b,
0x1f, 0xe6, 0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
]);
// Asserts that the size of the given key (in chars or bytes) is within 10% of
// the expected size.
function assertApproximateSize(key, expectedSize) {
const u = typeof key === 'string' ? 'chars' : 'bytes';
const min = Math.floor(0.9 * expectedSize);
const max = Math.ceil(1.1 * expectedSize);
assert(key.length >= min,
`Key (${key.length} ${u}) is shorter than expected (${min} ${u})`);
assert(key.length <= max,
`Key (${key.length} ${u}) is longer than expected (${max} ${u})`);
}
// Tests that a key pair can be used for encryption / decryption.
function testEncryptDecrypt(publicKey, privateKey) {
const message = 'Hello Node.js world!';
const plaintext = Buffer.from(message, 'utf8');
for (const key of [publicKey, privateKey]) {
const ciphertext = publicEncrypt(key, plaintext);
const received = privateDecrypt(privateKey, ciphertext);
assert.strictEqual(received.toString('utf8'), message);
}
}
// Tests that a key pair can be used for signing / verification.
function testSignVerify(publicKey, privateKey) {
const message = Buffer.from('Hello Node.js world!');
function oldSign(algo, data, key) {
return createSign(algo).update(data).sign(key);
}
function oldVerify(algo, data, key, signature) {
return createVerify(algo).update(data).verify(key, signature);
}
for (const signFn of [sign, oldSign]) {
const signature = signFn('SHA256', message, privateKey);
for (const verifyFn of [verify, oldVerify]) {
for (const key of [publicKey, privateKey]) {
const okay = verifyFn('SHA256', message, key, signature);
assert(okay);
}
}
}
}
// Constructs a regular expression for a PEM-encoded key with the given label.
function getRegExpForPEM(label, cipher) {
const head = `\\-\\-\\-\\-\\-BEGIN ${label}\\-\\-\\-\\-\\-`;
const rfc1421Header = cipher == null ? '' :
`\nProc-Type: 4,ENCRYPTED\nDEK-Info: ${cipher},[^\n]+\n`;
const body = '([a-zA-Z0-9\\+/=]{64}\n)*[a-zA-Z0-9\\+/=]{1,64}';
const end = `\\-\\-\\-\\-\\-END ${label}\\-\\-\\-\\-\\-`;
return new RegExp(`^${head}${rfc1421Header}\n${body}\n${end}\n$`);
}
const pkcs1PubExp = getRegExpForPEM('RSA PUBLIC KEY');
const pkcs1PrivExp = getRegExpForPEM('RSA PRIVATE KEY');
const pkcs1EncExp = (cipher) => getRegExpForPEM('RSA PRIVATE KEY', cipher);
const spkiExp = getRegExpForPEM('PUBLIC KEY');
const pkcs8Exp = getRegExpForPEM('PRIVATE KEY');
const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY');
const sec1Exp = getRegExpForPEM('EC PRIVATE KEY');
const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher);
module.exports = {
modp2buf,
assertApproximateSize,
testEncryptDecrypt,
testSignVerify,
pkcs1PubExp,
pkcs1PrivExp,
pkcs1EncExp, // used once
spkiExp,
pkcs8Exp, // used once
pkcs8EncExp, // used once
sec1Exp,
sec1EncExp,
};

View File

@@ -0,0 +1,183 @@
'use strict';
const common = require('../common');
const spawn = require('child_process').spawn;
const BREAK_MESSAGE = new RegExp('(?:' + [
'assert', 'break', 'break on start', 'debugCommand',
'exception', 'other', 'promiseRejection', 'step',
].join('|') + ') in', 'i');
let TIMEOUT = common.platformTimeout(5000);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to receive
// the outputs from the client.
// https://github.com/nodejs/build/issues/3014
TIMEOUT = common.platformTimeout(15000);
}
function isPreBreak(output) {
return /Break on start/.test(output) && /1 \(function \(exports/.test(output);
}
function startCLI(args, flags = [], spawnOpts = {}) {
let stderrOutput = '';
const child =
spawn(process.execPath, [...flags, 'inspect', ...args], spawnOpts);
const outputBuffer = [];
function bufferOutput(chunk) {
if (this === child.stderr) {
stderrOutput += chunk;
}
outputBuffer.push(chunk);
}
function getOutput() {
return outputBuffer.join('\n').replaceAll('\b', '');
}
child.stdout.setEncoding('utf8');
child.stdout.on('data', bufferOutput);
child.stderr.setEncoding('utf8');
child.stderr.on('data', bufferOutput);
if (process.env.VERBOSE === '1') {
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
}
return {
flushOutput() {
const output = this.output;
outputBuffer.length = 0;
return output;
},
waitFor(pattern) {
function checkPattern(str) {
if (Array.isArray(pattern)) {
return pattern.every((p) => p.test(str));
}
return pattern.test(str);
}
return new Promise((resolve, reject) => {
function checkOutput() {
if (checkPattern(getOutput())) {
tearDown();
resolve();
}
}
function onChildClose(code, signal) {
tearDown();
let message = 'Child exited';
if (code) {
message += `, code ${code}`;
}
if (signal) {
message += `, signal ${signal}`;
}
message += ` while waiting for ${pattern}; found: ${this.output}`;
if (stderrOutput) {
message += `\n STDERR: ${stderrOutput}`;
}
reject(new Error(message));
}
const timer = setTimeout(() => {
tearDown();
reject(new Error([
`Timeout (${TIMEOUT}) while waiting for ${pattern}`,
`found: ${this.output}`,
].join('; ')));
}, TIMEOUT);
function tearDown() {
clearTimeout(timer);
child.stdout.removeListener('data', checkOutput);
child.removeListener('close', onChildClose);
}
child.on('close', onChildClose);
child.stdout.on('data', checkOutput);
checkOutput();
});
},
waitForPrompt() {
return this.waitFor(/>\s+$/);
},
async waitForInitialBreak() {
await this.waitFor(/break (?:on start )?in/i);
if (isPreBreak(this.output)) {
await this.command('next', false);
return this.waitFor(/break in/);
}
},
get breakInfo() {
const output = this.output;
const breakMatch =
output.match(/(step |break (?:on start )?)in ([^\n]+):(\d+)\n/i);
if (breakMatch === null) {
throw new Error(
`Could not find breakpoint info in ${JSON.stringify(output)}`);
}
return { filename: breakMatch[2], line: +breakMatch[3] };
},
ctrlC() {
return this.command('.interrupt');
},
get output() {
return getOutput();
},
get rawOutput() {
return outputBuffer.join('').toString();
},
parseSourceLines() {
return getOutput().split('\n')
.map((line) => line.match(/(?:\*|>)?\s*(\d+)/))
.filter((match) => match !== null)
.map((match) => +match[1]);
},
writeLine(input, flush = true) {
if (flush) {
this.flushOutput();
}
if (process.env.VERBOSE === '1') {
process.stderr.write(`< ${input}\n`);
}
child.stdin.write(input);
child.stdin.write('\n');
},
command(input, flush = true) {
this.writeLine(input, flush);
return this.waitForPrompt();
},
stepCommand(input) {
this.writeLine(input, true);
return this
.waitFor(BREAK_MESSAGE)
.then(() => this.waitForPrompt());
},
quit() {
return new Promise((resolve) => {
child.stdin.end();
child.on('close', resolve);
});
},
};
}
module.exports = startCLI;

View File

@@ -0,0 +1,341 @@
'use strict';
const assert = require('assert');
const os = require('os');
const { isIP } = require('net');
const types = {
A: 1,
AAAA: 28,
NS: 2,
CNAME: 5,
SOA: 6,
PTR: 12,
MX: 15,
TXT: 16,
ANY: 255,
CAA: 257,
};
const classes = {
IN: 1,
};
// Naïve DNS parser/serializer.
function readDomainFromPacket(buffer, offset) {
assert.ok(offset < buffer.length);
const length = buffer[offset];
if (length === 0) {
return { nread: 1, domain: '' };
} else if ((length & 0xC0) === 0) {
offset += 1;
const chunk = buffer.toString('ascii', offset, offset + length);
// Read the rest of the domain.
const { nread, domain } = readDomainFromPacket(buffer, offset + length);
return {
nread: 1 + length + nread,
domain: domain ? `${chunk}.${domain}` : chunk,
};
}
// Pointer to another part of the packet.
assert.strictEqual(length & 0xC0, 0xC0);
// eslint-disable-next-line @stylistic/js/space-infix-ops, @stylistic/js/space-unary-ops
const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000;
return {
nread: 2,
domain: readDomainFromPacket(buffer, pointeeOffset),
};
}
function parseDNSPacket(buffer) {
assert.ok(buffer.length > 12);
const parsed = {
id: buffer.readUInt16BE(0),
flags: buffer.readUInt16BE(2),
};
const counts = [
['questions', buffer.readUInt16BE(4)],
['answers', buffer.readUInt16BE(6)],
['authorityAnswers', buffer.readUInt16BE(8)],
['additionalRecords', buffer.readUInt16BE(10)],
];
let offset = 12;
for (const [ sectionName, count ] of counts) {
parsed[sectionName] = [];
for (let i = 0; i < count; ++i) {
const { nread, domain } = readDomainFromPacket(buffer, offset);
offset += nread;
const type = buffer.readUInt16BE(offset);
const rr = {
domain,
cls: buffer.readUInt16BE(offset + 2),
};
offset += 4;
for (const name in types) {
if (types[name] === type)
rr.type = name;
}
if (sectionName !== 'questions') {
rr.ttl = buffer.readInt32BE(offset);
const dataLength = buffer.readUInt16BE(offset);
offset += 6;
switch (type) {
case types.A:
assert.strictEqual(dataLength, 4);
rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` +
`${buffer[offset + 2]}.${buffer[offset + 3]}`;
break;
case types.AAAA:
assert.strictEqual(dataLength, 16);
rr.address = buffer.toString('hex', offset, offset + 16)
.replace(/(.{4}(?!$))/g, '$1:');
break;
case types.TXT:
{
let position = offset;
rr.entries = [];
while (position < offset + dataLength) {
const txtLength = buffer[offset];
rr.entries.push(buffer.toString('utf8',
position + 1,
position + 1 + txtLength));
position += 1 + txtLength;
}
assert.strictEqual(position, offset + dataLength);
break;
}
case types.MX:
{
rr.priority = buffer.readInt16BE(buffer, offset);
offset += 2;
const { nread, domain } = readDomainFromPacket(buffer, offset);
rr.exchange = domain;
assert.strictEqual(nread, dataLength);
break;
}
case types.NS:
case types.CNAME:
case types.PTR:
{
const { nread, domain } = readDomainFromPacket(buffer, offset);
rr.value = domain;
assert.strictEqual(nread, dataLength);
break;
}
case types.SOA:
{
const mname = readDomainFromPacket(buffer, offset);
const rname = readDomainFromPacket(buffer, offset + mname.nread);
rr.nsname = mname.domain;
rr.hostmaster = rname.domain;
const trailerOffset = offset + mname.nread + rname.nread;
rr.serial = buffer.readUInt32BE(trailerOffset);
rr.refresh = buffer.readUInt32BE(trailerOffset + 4);
rr.retry = buffer.readUInt32BE(trailerOffset + 8);
rr.expire = buffer.readUInt32BE(trailerOffset + 12);
rr.minttl = buffer.readUInt32BE(trailerOffset + 16);
assert.strictEqual(trailerOffset + 20, dataLength);
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}
offset += dataLength;
}
parsed[sectionName].push(rr);
assert.ok(offset <= buffer.length);
}
}
assert.strictEqual(offset, buffer.length);
return parsed;
}
function writeIPv6(ip) {
const parts = ip.replace(/^:|:$/g, '').split(':');
const buf = Buffer.alloc(16);
let offset = 0;
for (const part of parts) {
if (part === '') {
offset += 16 - 2 * (parts.length - 1);
} else {
buf.writeUInt16BE(parseInt(part, 16), offset);
offset += 2;
}
}
return buf;
}
function writeDomainName(domain) {
return Buffer.concat(domain.split('.').map((label) => {
assert(label.length < 64);
return Buffer.concat([
Buffer.from([label.length]),
Buffer.from(label, 'ascii'),
]);
}).concat([Buffer.alloc(1)]));
}
function writeDNSPacket(parsed) {
const buffers = [];
const kStandardResponseFlags = 0x8180;
buffers.push(new Uint16Array([
parsed.id,
parsed.flags ?? kStandardResponseFlags,
parsed.questions?.length,
parsed.answers?.length,
parsed.authorityAnswers?.length,
parsed.additionalRecords?.length,
]));
for (const q of parsed.questions) {
assert(types[q.type]);
buffers.push(writeDomainName(q.domain));
buffers.push(new Uint16Array([
types[q.type],
q.cls === undefined ? classes.IN : q.cls,
]));
}
for (const rr of [].concat(parsed.answers,
parsed.authorityAnswers,
parsed.additionalRecords)) {
if (!rr) continue;
assert(types[rr.type]);
buffers.push(writeDomainName(rr.domain));
buffers.push(new Uint16Array([
types[rr.type],
rr.cls === undefined ? classes.IN : rr.cls,
]));
buffers.push(new Int32Array([rr.ttl]));
const rdLengthBuf = new Uint16Array(1);
buffers.push(rdLengthBuf);
switch (rr.type) {
case 'A':
rdLengthBuf[0] = 4;
buffers.push(new Uint8Array(rr.address.split('.')));
break;
case 'AAAA':
rdLengthBuf[0] = 16;
buffers.push(writeIPv6(rr.address));
break;
case 'TXT': {
const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b);
// Total length of all strings + 1 byte each for their lengths.
rdLengthBuf[0] = rr.entries.length + total;
for (const txt of rr.entries) {
buffers.push(new Uint8Array([Buffer.byteLength(txt)]));
buffers.push(Buffer.from(txt));
}
break;
}
case 'MX':
rdLengthBuf[0] = 2;
buffers.push(new Uint16Array([rr.priority]));
// fall through
case 'NS':
case 'CNAME':
case 'PTR':
{
const domain = writeDomainName(rr.exchange || rr.value);
rdLengthBuf[0] += domain.length;
buffers.push(domain);
break;
}
case 'SOA':
{
const mname = writeDomainName(rr.nsname);
const rname = writeDomainName(rr.hostmaster);
rdLengthBuf[0] = mname.length + rname.length + 20;
buffers.push(mname, rname);
buffers.push(new Uint32Array([
rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl,
]));
break;
}
case 'CAA':
{
rdLengthBuf[0] = 5 + rr.issue.length + 2;
buffers.push(Buffer.from([Number(rr.critical)]));
buffers.push(Buffer.from([Number(5)]));
buffers.push(Buffer.from('issue' + rr.issue));
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}
}
return Buffer.concat(buffers.map((typedArray) => {
const buf = Buffer.from(typedArray.buffer,
typedArray.byteOffset,
typedArray.byteLength);
if (os.endianness() === 'LE') {
if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16();
if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32();
}
return buf;
}));
}
const mockedErrorCode = 'ENOTFOUND';
const mockedSysCall = 'getaddrinfo';
function errorLookupMock(code = mockedErrorCode, syscall = mockedSysCall) {
return function lookupWithError(hostname, dnsopts, cb) {
const err = new Error(`${syscall} ${code} ${hostname}`);
err.code = code;
err.errno = code;
err.syscall = syscall;
err.hostname = hostname;
cb(err);
};
}
function createMockedLookup(...addresses) {
addresses = addresses.map((address) => ({ address: address, family: isIP(address) }));
// Create a DNS server which replies with a AAAA and a A record for the same host
return function lookup(hostname, options, cb) {
if (options.all === true) {
process.nextTick(() => {
cb(null, addresses);
});
return;
}
process.nextTick(() => {
cb(null, addresses[0].address, addresses[0].family);
});
};
}
module.exports = {
types,
classes,
writeDNSPacket,
parseDNSPacket,
errorLookupMock,
mockedErrorCode,
mockedSysCall,
createMockedLookup,
};

View File

@@ -0,0 +1,59 @@
'use strict';
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');
const fixturesDir = path.join(__dirname, '..', 'fixtures');
function fixturesPath(...args) {
return path.join(fixturesDir, ...args);
}
function fixturesFileURL(...args) {
return pathToFileURL(fixturesPath(...args));
}
function readFixtureSync(args, enc) {
if (Array.isArray(args))
return fs.readFileSync(fixturesPath(...args), enc);
return fs.readFileSync(fixturesPath(args), enc);
}
function readFixtureKey(name, enc) {
return fs.readFileSync(fixturesPath('keys', name), enc);
}
function readFixtureKeys(enc, ...names) {
return names.map((name) => readFixtureKey(name, enc));
}
// This should be in sync with test/fixtures/utf8_test_text.txt.
// We copy them here as a string because this is supposed to be used
// in fs API tests.
const utf8TestText = '永和九年,嵗在癸丑,暮春之初,會於會稽山隂之蘭亭,脩稧事也。' +
'羣賢畢至,少長咸集。此地有崇山峻領,茂林脩竹;又有清流激湍,' +
'暎帶左右。引以為流觴曲水,列坐其次。雖無絲竹管弦之盛,一觴一詠,' +
'亦足以暢敘幽情。是日也,天朗氣清,恵風和暢;仰觀宇宙之大,' +
'俯察品類之盛;所以遊目騁懐,足以極視聽之娛,信可樂也。夫人之相與,' +
'俯仰一世,或取諸懐抱,悟言一室之內,或因寄所託,放浪形骸之外。' +
'雖趣舎萬殊,靜躁不同,當其欣扵所遇,暫得扵己,怏然自足,' +
'不知老之將至。及其所之既惓,情隨事遷,感慨係之矣。向之所欣,' +
'俛仰之閒以為陳跡,猶不能不以之興懐;況脩短隨化,終期扵盡。' +
'古人云:「死生亦大矣。」豈不痛哉!每攬昔人興感之由,若合一契,' +
'未嘗不臨文嗟悼,不能喻之扵懐。固知一死生為虛誕,齊彭殤為妄作。' +
'後之視今,亦由今之視昔,悲夫!故列敘時人,錄其所述,雖世殊事異,' +
'所以興懐,其致一也。後之攬者,亦將有感扵斯文。';
module.exports = {
fixturesDir,
path: fixturesPath,
fileURL: fixturesFileURL,
readSync: readFixtureSync,
readKey: readFixtureKey,
readKeys: readFixtureKeys,
utf8TestText,
get utf8TestTextPath() {
return fixturesPath('utf8_test_text.txt');
},
};

View File

@@ -0,0 +1,5 @@
import fixtures from "./fixtures.js";
const { fixturesDir, path, fileURL, readSync, readKey } = fixtures;
export { fileURL, fixturesDir, path, readKey, readSync };

View File

@@ -0,0 +1,192 @@
'use strict';
const wait = require('timers/promises').setTimeout;
const assert = require('assert');
const common = require('../common');
const gcTrackerMap = new WeakMap();
const gcTrackerTag = 'NODE_TEST_COMMON_GC_TRACKER';
/**
* Installs a garbage collection listener for the specified object.
* Uses async_hooks for GC tracking, which may affect test functionality.
* A full setImmediate() invocation passes between a global.gc() call and the listener being invoked.
* @param {object} obj - The target object to track for garbage collection.
* @param {object} gcListener - The listener object containing the ongc callback.
* @param {Function} gcListener.ongc - The function to call when the target object is garbage collected.
*/
function onGC(obj, gcListener) {
const async_hooks = require('async_hooks');
const onGcAsyncHook = async_hooks.createHook({
init: common.mustCallAtLeast(function(id, type) {
if (this.trackedId === undefined) {
assert.strictEqual(type, gcTrackerTag);
this.trackedId = id;
}
}),
destroy(id) {
assert.notStrictEqual(this.trackedId, -1);
if (id === this.trackedId) {
this.gcListener.ongc();
onGcAsyncHook.disable();
}
},
}).enable();
onGcAsyncHook.gcListener = gcListener;
gcTrackerMap.set(obj, new async_hooks.AsyncResource(gcTrackerTag));
obj = null;
}
/**
* Repeatedly triggers garbage collection until a specified condition is met or a maximum number of attempts is reached.
* @param {string|Function} [name] - Optional name, used in the rejection message if the condition is not met.
* @param {Function} condition - A function that returns true when the desired condition is met.
* @returns {Promise} A promise that resolves when the condition is met, or rejects after 10 failed attempts.
*/
function gcUntil(name, condition) {
if (typeof name === 'function') {
condition = name;
name = undefined;
}
return new Promise((resolve, reject) => {
let count = 0;
function gcAndCheck() {
setImmediate(() => {
count++;
global.gc();
if (condition()) {
resolve();
} else if (count < 10) {
gcAndCheck();
} else {
reject(name === undefined ? undefined : 'Test ' + name + ' failed');
}
});
}
gcAndCheck();
});
}
// This function can be used to check if an object factor leaks or not,
// but it needs to be used with care:
// 1. The test should be set up with an ideally small
// --max-old-space-size or --max-heap-size, which combined with
// the maxCount parameter can reproduce a leak of the objects
// created by fn().
// 2. This works under the assumption that if *none* of the objects
// created by fn() can be garbage-collected, the test would crash due
// to OOM.
// 3. If *any* of the objects created by fn() can be garbage-collected,
// it is considered leak-free. The FinalizationRegistry is used to
// terminate the test early once we detect any of the object is
// garbage-collected to make the test less prone to false positives.
// This may be especially important for memory management relying on
// emphemeron GC which can be inefficient to deal with extremely fast
// heap growth.
// Note that this can still produce false positives. When the test using
// this function still crashes due to OOM, inspect the heap to confirm
// if a leak is present (e.g. using heap snapshots).
// The generateSnapshotAt parameter can be used to specify a count
// interval to create the heap snapshot which may enforce a more thorough GC.
// This can be tried for code paths that require it for the GC to catch up
// with heap growth. However this type of forced GC can be in conflict with
// other logic in V8 such as bytecode aging, and it can slow down the test
// significantly, so it should be used scarcely and only as a last resort.
async function checkIfCollectable(
fn, maxCount = 4096, generateSnapshotAt = Infinity, logEvery = 128) {
let anyFinalized = false;
let count = 0;
const f = new FinalizationRegistry(() => {
anyFinalized = true;
});
async function createObject() {
const obj = await fn();
f.register(obj);
if (count++ < maxCount && !anyFinalized) {
setImmediate(createObject, 1);
}
// This can force a more thorough GC, but can slow the test down
// significantly in a big heap. Use it with care.
if (count % generateSnapshotAt === 0) {
// XXX(joyeecheung): This itself can consume a bit of JS heap memory,
// but the other alternative writeHeapSnapshot can run into disk space
// not enough problems in the CI & be slower depending on file system.
// Just do this for now as long as it works and only invent some
// internal voodoo when we absolutely have no other choice.
require('v8').getHeapSnapshot().pause().read();
console.log(`Generated heap snapshot at ${count}`);
}
if (count % logEvery === 0) {
console.log(`Created ${count} objects`);
}
if (anyFinalized) {
console.log(`Found finalized object at ${count}, stop testing`);
}
}
createObject();
}
// Repeat an operation and give GC some breathing room at every iteration.
async function runAndBreathe(fn, repeat, waitTime = 20) {
for (let i = 0; i < repeat; i++) {
await fn();
await wait(waitTime);
}
}
/**
* This requires --expose-internals.
* This function can be used to check if an object factory leaks or not by
* iterating over the heap and count objects with the specified class
* (which is checked by looking up the prototype chain).
* @param {(i: number) => number} fn The factory receiving iteration count
* and returning number of objects created. The return value should be
* precise otherwise false negatives can be produced.
* @param {Function} ctor The constructor of the objects being counted.
* @param {number} count Number of iterations that this check should be done
* @param {number} waitTime Optional breathing time for GC.
*/
async function checkIfCollectableByCounting(fn, ctor, count, waitTime = 20) {
const { queryObjects } = require('v8');
const { name } = ctor;
const initialCount = queryObjects(ctor, { format: 'count' });
console.log(`Initial count of ${name}: ${initialCount}`);
let totalCreated = 0;
for (let i = 0; i < count; ++i) {
const created = await fn(i);
totalCreated += created;
console.log(`#${i}: created ${created} ${name}, total ${totalCreated}`);
await wait(waitTime); // give GC some breathing room.
const currentCount = queryObjects(ctor, { format: 'count' });
const collected = totalCreated - (currentCount - initialCount);
console.log(`#${i}: counted ${currentCount} ${name}, collected ${collected}`);
if (collected > 0) {
console.log(`Detected ${collected} collected ${name}, finish early`);
return;
}
}
await wait(waitTime); // give GC some breathing room.
const currentCount = queryObjects(ctor, { format: 'count' });
const collected = totalCreated - (currentCount - initialCount);
console.log(`Last count: counted ${currentCount} ${name}, collected ${collected}`);
// Some objects with the prototype can be collected.
if (collected > 0) {
console.log(`Detected ${collected} collected ${name}`);
return;
}
throw new Error(`${name} cannot be collected`);
}
module.exports = {
checkIfCollectable,
runAndBreathe,
checkIfCollectableByCounting,
onGC,
gcUntil,
};

View File

@@ -0,0 +1,146 @@
'use strict';
const intrinsics = new Set([
'Object',
'Function',
'Array',
'Number',
'parseFloat',
'parseInt',
'Infinity',
'NaN',
'undefined',
'Boolean',
'String',
'Symbol',
'Date',
'Promise',
'RegExp',
'Error',
'AggregateError',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
'globalThis',
'JSON',
'Math',
'Intl',
'ArrayBuffer',
'Uint8Array',
'Int8Array',
'Uint16Array',
'Int16Array',
'Uint32Array',
'Int32Array',
'Float32Array',
'Float64Array',
'Uint8ClampedArray',
'BigUint64Array',
'BigInt64Array',
'DataView',
'Map',
'BigInt',
'Set',
'WeakMap',
'WeakSet',
'Proxy',
'Reflect',
'ShadowRealm',
'FinalizationRegistry',
'WeakRef',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'escape',
'unescape',
'eval',
'isFinite',
'isNaN',
'SharedArrayBuffer',
'Atomics',
'WebAssembly',
'Iterator',
]);
if (global.gc) {
intrinsics.add('gc');
}
// v8 exposes console in the global scope.
intrinsics.add('console');
const webIdlExposedWildcard = new Set([
'DOMException',
'TextEncoder',
'TextDecoder',
'AbortController',
'AbortSignal',
'CustomEvent',
'EventTarget',
'Event',
'URL',
'URLSearchParams',
'ReadableStream',
'ReadableStreamDefaultReader',
'ReadableStreamBYOBReader',
'ReadableStreamBYOBRequest',
'ReadableByteStreamController',
'ReadableStreamDefaultController',
'TransformStream',
'TransformStreamDefaultController',
'WritableStream',
'WritableStreamDefaultWriter',
'WritableStreamDefaultController',
'ByteLengthQueuingStrategy',
'CountQueuingStrategy',
'TextEncoderStream',
'TextDecoderStream',
'CompressionStream',
'DecompressionStream',
]);
const webIdlExposedWindow = new Set([
'console',
'BroadcastChannel',
'queueMicrotask',
'structuredClone',
'MessageChannel',
'MessagePort',
'MessageEvent',
'clearInterval',
'clearTimeout',
'setInterval',
'setTimeout',
'atob',
'btoa',
'Blob',
'Performance',
'performance',
'fetch',
'FormData',
'Headers',
'Request',
'Response',
'WebSocket',
'EventSource',
'CloseEvent',
]);
const nodeGlobals = new Set([
'process',
'global',
'Buffer',
'clearImmediate',
'setImmediate',
]);
module.exports = {
intrinsics,
webIdlExposedWildcard,
webIdlExposedWindow,
nodeGlobals,
};

View File

@@ -0,0 +1,329 @@
'use strict';
const assert = require('assert');
const util = require('util');
let _buildEmbedderGraph;
function buildEmbedderGraph() {
if (_buildEmbedderGraph) { return _buildEmbedderGraph(); }
let internalBinding;
try {
internalBinding = require('internal/test/binding').internalBinding;
} catch (e) {
console.error('The test must be run with `--expose-internals`');
throw e;
}
({ buildEmbedderGraph: _buildEmbedderGraph } = internalBinding('heap_utils'));
return _buildEmbedderGraph();
}
const { getHeapSnapshot } = require('v8');
function createJSHeapSnapshot(stream = getHeapSnapshot()) {
stream.pause();
const dump = JSON.parse(stream.read());
const meta = dump.snapshot.meta;
const nodes =
readHeapInfo(dump.nodes, meta.node_fields, meta.node_types, dump.strings);
const edges =
readHeapInfo(dump.edges, meta.edge_fields, meta.edge_types, dump.strings);
for (const node of nodes) {
node.incomingEdges = [];
node.outgoingEdges = [];
}
let fromNodeIndex = 0;
let edgeIndex = 0;
for (const { type, name_or_index, to_node } of edges) {
while (edgeIndex === nodes[fromNodeIndex].edge_count) {
edgeIndex = 0;
fromNodeIndex++;
}
const toNode = nodes[to_node / meta.node_fields.length];
const fromNode = nodes[fromNodeIndex];
const edge = {
type,
to: toNode,
from: fromNode,
name: typeof name_or_index === 'string' ? name_or_index : null,
};
toNode.incomingEdges.push(edge);
fromNode.outgoingEdges.push(edge);
edgeIndex++;
}
for (const node of nodes) {
assert.strictEqual(node.edge_count, node.outgoingEdges.length,
`${node.edge_count} !== ${node.outgoingEdges.length}`);
}
return nodes;
}
function readHeapInfo(raw, fields, types, strings) {
const items = [];
for (let i = 0; i < raw.length; i += fields.length) {
const item = {};
for (let j = 0; j < fields.length; j++) {
const name = fields[j];
let type = types[j];
if (Array.isArray(type)) {
item[name] = type[raw[i + j]];
} else if (name === 'name_or_index') { // type === 'string_or_number'
if (item.type === 'element' || item.type === 'hidden')
type = 'number';
else
type = 'string';
}
if (type === 'string') {
item[name] = strings[raw[i + j]];
} else if (type === 'number' || type === 'node') {
item[name] = raw[i + j];
}
}
items.push(item);
}
return items;
}
function inspectNode(snapshot) {
return util.inspect(snapshot, { depth: 4 });
}
function isEdge(edge, { node_name, edge_name }) {
if (edge_name !== undefined && edge.name !== edge_name) {
return false;
}
// From our internal embedded graph
if (edge.to.value) {
if (edge.to.value.constructor.name !== node_name) {
return false;
}
} else if (edge.to.name !== node_name) {
return false;
}
return true;
}
class State {
constructor(stream) {
this.snapshot = createJSHeapSnapshot(stream);
this.embedderGraph = buildEmbedderGraph();
}
// Validate the v8 heap snapshot
validateSnapshot(rootName, expected, { loose = false } = {}) {
const rootNodes = this.snapshot.filter(
(node) => node.name === rootName && node.type !== 'string');
if (loose) {
assert(rootNodes.length >= expected.length,
`Expect to find at least ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
} else {
assert.strictEqual(
rootNodes.length, expected.length,
`Expect to find ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
}
for (const expectation of expected) {
if (expectation.children) {
for (const expectedEdge of expectation.children) {
const check = typeof expectedEdge === 'function' ? expectedEdge :
(edge) => (isEdge(edge, expectedEdge));
const hasChild = rootNodes.some(
(node) => node.outgoingEdges.some(check),
);
// Don't use assert with a custom message here. Otherwise the
// inspection in the message is done eagerly and wastes a lot of CPU
// time.
if (!hasChild) {
throw new Error(
'expected to find child ' +
`${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`);
}
}
}
if (expectation.detachedness !== undefined) {
const matchedNodes = rootNodes.filter(
(node) => node.detachedness === expectation.detachedness);
if (loose) {
assert(matchedNodes.length >= rootNodes.length,
`Expect to find at least ${rootNodes.length} with ` +
`detachedness ${expectation.detachedness}, ` +
`found ${matchedNodes.length}`);
} else {
assert.strictEqual(
matchedNodes.length, rootNodes.length,
`Expect to find ${rootNodes.length} with detachedness ` +
`${expectation.detachedness}, found ${matchedNodes.length}`);
}
}
}
}
// Validate our internal embedded graph representation
validateGraph(rootName, expected, { loose = false } = {}) {
const rootNodes = this.embedderGraph.filter(
(node) => node.name === rootName,
);
if (loose) {
assert(rootNodes.length >= expected.length,
`Expect to find at least ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
} else {
assert.strictEqual(
rootNodes.length, expected.length,
`Expect to find ${expected.length} '${rootName}', ` +
`found ${rootNodes.length}`);
}
for (const expectation of expected) {
if (expectation.children) {
for (const expectedEdge of expectation.children) {
const check = typeof expectedEdge === 'function' ? expectedEdge :
(edge) => (isEdge(edge, expectedEdge));
// Don't use assert with a custom message here. Otherwise the
// inspection in the message is done eagerly and wastes a lot of CPU
// time.
const hasChild = rootNodes.some(
(node) => node.edges.some(check),
);
if (!hasChild) {
throw new Error(
'expected to find child ' +
`${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`);
}
}
}
}
}
validateSnapshotNodes(rootName, expected, { loose = false } = {}) {
this.validateSnapshot(rootName, expected, { loose });
this.validateGraph(rootName, expected, { loose });
}
}
function recordState(stream = undefined) {
return new State(stream);
}
function validateSnapshotNodes(...args) {
return recordState().validateSnapshotNodes(...args);
}
/**
* A alternative heap snapshot validator that can be used to verify cppgc-managed nodes.
* Modified from
* https://chromium.googlesource.com/v8/v8/+/b00e995fb212737802810384ba2b868d0d92f7e5/test/unittests/heap/cppgc-js/unified-heap-snapshot-unittest.cc#134
* @param {string} rootName Name of the root node. Typically a class name used to filter all native nodes with
* this name. For cppgc-managed objects, this is typically the name configured by
* SET_CPPGC_NAME() prefixed with an additional "Node /" prefix e.g.
* "Node / ContextifyScript"
* @param {[{
* node_name?: string,
* edge_name?: string,
* node_type?: string,
* edge_type?: string,
* }]} retainingPath The retaining path specification to search from the root nodes.
* @returns {[object]} All the leaf nodes matching the retaining path specification. If none can be found,
* logs the nodes found in the last matching step of the path (if any), and throws an
* assertion error.
*/
function findByRetainingPath(rootName, retainingPath) {
const nodes = createJSHeapSnapshot();
let haystack = nodes.filter((n) => n.name === rootName && n.type !== 'string');
for (let i = 0; i < retainingPath.length; ++i) {
const expected = retainingPath[i];
const newHaystack = [];
for (const parent of haystack) {
for (let j = 0; j < parent.outgoingEdges.length; j++) {
const edge = parent.outgoingEdges[j];
// The strings are represented as { type: 'string', name: '<string content>' } in the snapshot.
// Ignore them or we'll poke into strings that are just referenced as names of real nodes,
// unless the caller is specifically looking for string nodes via `node_type`.
let match = (edge.to.type !== 'string');
if (expected.node_type) {
match = (edge.to.type === expected.node_type);
}
if (expected.node_name && edge.to.name !== expected.node_name) {
match = false;
}
if (expected.edge_name && edge.name !== expected.edge_name) {
match = false;
}
if (expected.edge_type && edge.type !== expected.type) {
match = false;
}
if (match) {
newHaystack.push(edge.to);
}
}
}
if (newHaystack.length === 0) {
const format = (val) => util.inspect(val, { breakLength: 128, depth: 3 });
console.error('#');
console.error('# Retaining path to search for:');
for (let j = 0; j < retainingPath.length; ++j) {
console.error(`# - '${format(retainingPath[j])}'${i === j ? '\t<--- not found' : ''}`);
}
console.error('#\n');
console.error('# Nodes found in the last step include:');
for (let j = 0; j < haystack.length; ++j) {
console.error(`# - '${format(haystack[j])}`);
}
assert.fail(`Could not find target edge ${format(expected)} in the heap snapshot.`);
}
haystack = newHaystack;
}
return haystack;
}
function getHeapSnapshotOptionTests() {
const fixtures = require('../common/fixtures');
const cases = [
{
options: { exposeInternals: true },
expected: [{
children: [
// We don't have anything special to test here yet
// because we don't use cppgc or embedder heap tracer.
{ edge_name: 'nonNumeric', node_name: 'test' },
],
}],
},
{
options: { exposeNumericValues: true },
expected: [{
children: [
{ edge_name: 'numeric', node_name: 'smi number' },
],
}],
},
];
return {
fixtures: fixtures.path('klass-with-fields.js'),
check(snapshot, expected) {
snapshot.validateSnapshot('Klass', expected, { loose: true });
},
cases,
};
}
module.exports = {
recordState,
validateSnapshotNodes,
findByRetainingPath,
getHeapSnapshotOptionTests,
};

View File

@@ -0,0 +1,32 @@
'use strict';
// Hijack stdout and stderr
const stdWrite = {};
function hijackStdWritable(name, listener) {
const stream = process[name];
const _write = stdWrite[name] = stream.write;
stream.writeTimes = 0;
stream.write = function(data, callback) {
try {
listener(data);
} catch (e) {
process.nextTick(() => { throw e; });
}
_write.call(stream, data, callback);
stream.writeTimes++;
};
}
function restoreWritable(name) {
process[name].write = stdWrite[name];
delete process[name].writeTimes;
}
module.exports = {
hijackStdout: hijackStdWritable.bind(null, 'stdout'),
hijackStderr: hijackStdWritable.bind(null, 'stderr'),
restoreStdout: restoreWritable.bind(null, 'stdout'),
restoreStderr: restoreWritable.bind(null, 'stderr'),
};

View File

@@ -0,0 +1,129 @@
'use strict';
// An HTTP/2 testing tool used to create mock frames for direct testing
// of HTTP/2 endpoints.
const kFrameData = Symbol('frame-data');
const FLAG_EOS = 0x1;
const FLAG_ACK = 0x1;
const FLAG_EOH = 0x4;
const FLAG_PADDED = 0x8;
const PADDING = Buffer.alloc(255);
const kClientMagic = Buffer.from('505249202a20485454502f322' +
'e300d0a0d0a534d0d0a0d0a', 'hex');
const kFakeRequestHeaders = Buffer.from('828684410f7777772e65' +
'78616d706c652e636f6d', 'hex');
const kFakeResponseHeaders = Buffer.from('4803333032580770726976617465611d' +
'4d6f6e2c203231204f63742032303133' +
'2032303a31333a323120474d546e1768' +
'747470733a2f2f7777772e6578616d70' +
'6c652e636f6d', 'hex');
function isUint32(val) {
return val >>> 0 === val;
}
function isUint24(val) {
return val >>> 0 === val && val <= 0xFFFFFF;
}
function isUint8(val) {
return val >>> 0 === val && val <= 0xFF;
}
function write32BE(array, pos, val) {
if (!isUint32(val))
throw new RangeError('val is not a 32-bit number');
array[pos++] = (val >> 24) & 0xff;
array[pos++] = (val >> 16) & 0xff;
array[pos++] = (val >> 8) & 0xff;
array[pos++] = val & 0xff;
}
function write24BE(array, pos, val) {
if (!isUint24(val))
throw new RangeError('val is not a 24-bit number');
array[pos++] = (val >> 16) & 0xff;
array[pos++] = (val >> 8) & 0xff;
array[pos++] = val & 0xff;
}
function write8(array, pos, val) {
if (!isUint8(val))
throw new RangeError('val is not an 8-bit number');
array[pos] = val;
}
class Frame {
constructor(length, type, flags, id) {
this[kFrameData] = Buffer.alloc(9);
write24BE(this[kFrameData], 0, length);
write8(this[kFrameData], 3, type);
write8(this[kFrameData], 4, flags);
write32BE(this[kFrameData], 5, id);
}
get data() {
return this[kFrameData];
}
}
class SettingsFrame extends Frame {
constructor(ack = false) {
let flags = 0;
if (ack)
flags |= FLAG_ACK;
super(0, 4, flags, 0);
}
}
class HeadersFrame extends Frame {
constructor(id, payload, padlen = 0, final = false) {
let len = payload.length;
let flags = FLAG_EOH;
if (final) flags |= FLAG_EOS;
const buffers = [payload];
if (padlen > 0) {
buffers.unshift(Buffer.from([padlen]));
buffers.push(PADDING.slice(0, padlen));
len += padlen + 1;
flags |= FLAG_PADDED;
}
super(len, 1, flags, id);
buffers.unshift(this[kFrameData]);
this[kFrameData] = Buffer.concat(buffers);
}
}
class PingFrame extends Frame {
constructor(ack = false) {
const buffers = [Buffer.alloc(8)];
super(8, 6, ack ? 1 : 0, 0);
buffers.unshift(this[kFrameData]);
this[kFrameData] = Buffer.concat(buffers);
}
}
class AltSvcFrame extends Frame {
constructor(size) {
const buffers = [Buffer.alloc(size)];
super(size, 10, 0, 0);
buffers.unshift(this[kFrameData]);
this[kFrameData] = Buffer.concat(buffers);
}
}
module.exports = {
Frame,
AltSvcFrame,
HeadersFrame,
SettingsFrame,
PingFrame,
kFakeRequestHeaders,
kFakeResponseHeaders,
kClientMagic,
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const common = require("./index.js");
const {
allowGlobals,
buildType,
canCreateSymLink,
checkoutEOL,
childShouldThrowAndAbort,
createZeroFilledFile,
enoughTestMem,
escapePOSIXShell,
expectsError,
expectWarning,
getArrayBufferViews,
getBufferSources,
getTTYfd,
hasCrypto,
hasIntl,
hasIPv6,
hasMultiLocalhost,
isAIX,
isAlive,
isDumbTerminal,
isFreeBSD,
isIBMi,
isLinux,
isLinuxPPCBE,
isMainThread,
isOpenBSD,
isMacOS,
isSunOS,
isWindows,
localIPv6Hosts,
mustCall,
mustCallAtLeast,
mustNotCall,
mustNotMutateObjectDeep,
mustSucceed,
nodeProcessAborted,
opensslCli,
parseTestFlags,
PIPE,
platformTimeout,
printSkipMessage,
runWithInvalidFD,
skip,
skipIf32Bits,
skipIfDumbTerminal,
skipIfEslintMissing,
skipIfInspectorDisabled,
spawnPromisified,
} = common;
const getPort = () => common.PORT;
export {
allowGlobals,
buildType,
canCreateSymLink,
checkoutEOL,
childShouldThrowAndAbort,
createRequire,
createZeroFilledFile,
enoughTestMem,
escapePOSIXShell,
expectsError,
expectWarning,
getArrayBufferViews,
getBufferSources,
getPort,
getTTYfd,
hasCrypto,
hasIntl,
hasIPv6,
hasMultiLocalhost,
isAIX,
isAlive,
isDumbTerminal,
isFreeBSD,
isIBMi,
isLinux,
isLinuxPPCBE,
isMacOS,
isMainThread,
isOpenBSD,
isSunOS,
isWindows,
localIPv6Hosts,
mustCall,
mustCallAtLeast,
mustNotCall,
mustNotMutateObjectDeep,
mustSucceed,
nodeProcessAborted,
opensslCli,
parseTestFlags,
PIPE,
platformTimeout,
printSkipMessage,
runWithInvalidFD,
skip,
skipIf32Bits,
skipIfDumbTerminal,
skipIfEslintMissing,
skipIfInspectorDisabled,
spawnPromisified,
};

View File

@@ -0,0 +1,549 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const http = require('http');
const fixtures = require('../common/fixtures');
const { spawn } = require('child_process');
const { URL, pathToFileURL } = require('url');
const { EventEmitter } = require('events');
const _MAINSCRIPT = fixtures.path('loop.js');
const DEBUG = false;
const TIMEOUT = common.platformTimeout(15 * 1000);
function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) {
const args = [].concat(inspectorFlags);
if (scriptContents) {
args.push('-e', scriptContents);
} else {
args.push(scriptFile);
}
const child = spawn(process.execPath, args);
const handler = tearDown.bind(null, child);
process.on('exit', handler);
process.on('uncaughtException', handler);
process.on('unhandledRejection', handler);
process.on('SIGINT', handler);
return child;
}
function makeBufferingDataCallback(dataCallback) {
let buffer = Buffer.alloc(0);
return (data) => {
const newData = Buffer.concat([buffer, data]);
const str = newData.toString('utf8');
const lines = str.replace(/\r/g, '').split('\n');
if (str.endsWith('\n'))
buffer = Buffer.alloc(0);
else
buffer = Buffer.from(lines.pop(), 'utf8');
for (const line of lines)
dataCallback(line);
};
}
function tearDown(child, err) {
child.kill();
if (err) {
console.error(err);
process.exit(1);
}
}
function parseWSFrame(buffer) {
// Protocol described in https://tools.ietf.org/html/rfc6455#section-5
let message = null;
if (buffer.length < 2)
return { length: 0, message };
if (buffer[0] === 0x88 && buffer[1] === 0x00) {
return { length: 2, message, closed: true };
}
assert.strictEqual(buffer[0], 0x81);
let dataLen = 0x7F & buffer[1];
let bodyOffset = 2;
if (buffer.length < bodyOffset + dataLen)
return 0;
if (dataLen === 126) {
dataLen = buffer.readUInt16BE(2);
bodyOffset = 4;
} else if (dataLen === 127) {
assert(buffer[2] === 0 && buffer[3] === 0, 'Inspector message too big');
dataLen = buffer.readUIntBE(4, 6);
bodyOffset = 10;
}
if (buffer.length < bodyOffset + dataLen)
return { length: 0, message };
const jsonPayload =
buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8');
try {
message = JSON.parse(jsonPayload);
} catch (e) {
console.error(`JSON.parse() failed for: ${jsonPayload}`);
throw e;
}
if (DEBUG)
console.log('[received]', JSON.stringify(message));
return { length: bodyOffset + dataLen, message };
}
function formatWSFrame(message) {
const messageBuf = Buffer.from(JSON.stringify(message));
const wsHeaderBuf = Buffer.allocUnsafe(16);
wsHeaderBuf.writeUInt8(0x81, 0);
let byte2 = 0x80;
const bodyLen = messageBuf.length;
let maskOffset = 2;
if (bodyLen < 126) {
byte2 = 0x80 + bodyLen;
} else if (bodyLen < 65536) {
byte2 = 0xFE;
wsHeaderBuf.writeUInt16BE(bodyLen, 2);
maskOffset = 4;
} else {
byte2 = 0xFF;
wsHeaderBuf.writeUInt32BE(bodyLen, 2);
wsHeaderBuf.writeUInt32BE(0, 6);
maskOffset = 10;
}
wsHeaderBuf.writeUInt8(byte2, 1);
wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset);
for (let i = 0; i < messageBuf.length; i++)
messageBuf[i] = messageBuf[i] ^ (1 << (i % 4));
return Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]);
}
class InspectorSession {
constructor(socket, instance) {
this._instance = instance;
this._socket = socket;
this._nextId = 1;
this._commandResponsePromises = new Map();
this._unprocessedNotifications = [];
this._notificationCallback = null;
this._scriptsIdsByUrl = new Map();
this._pausedDetails = null;
let buffer = Buffer.alloc(0);
socket.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
do {
const { length, message, closed } = parseWSFrame(buffer);
if (!length)
break;
if (closed) {
socket.write(Buffer.from([0x88, 0x00])); // WS close frame
}
buffer = buffer.slice(length);
if (message)
this._onMessage(message);
} while (true);
});
this._terminationPromise = new Promise((resolve) => {
socket.once('close', resolve);
});
}
waitForServerDisconnect() {
return this._terminationPromise;
}
async disconnect() {
this._socket.destroy();
return this.waitForServerDisconnect();
}
_onMessage(message) {
if (message.id) {
const { resolve, reject } = this._commandResponsePromises.get(message.id);
this._commandResponsePromises.delete(message.id);
if (message.result)
resolve(message.result);
else
reject(message.error);
} else {
if (message.method === 'Debugger.scriptParsed') {
const { scriptId, url } = message.params;
this._scriptsIdsByUrl.set(scriptId, url);
const fileUrl = url.startsWith('file:') ?
url : pathToFileURL(url).toString();
if (fileUrl === this.scriptURL().toString()) {
this.mainScriptId = scriptId;
}
}
if (message.method === 'Debugger.paused')
this._pausedDetails = message.params;
if (message.method === 'Debugger.resumed')
this._pausedDetails = null;
if (this._notificationCallback) {
// In case callback needs to install another
const callback = this._notificationCallback;
this._notificationCallback = null;
callback(message);
} else {
this._unprocessedNotifications.push(message);
}
}
}
unprocessedNotifications() {
return this._unprocessedNotifications;
}
_sendMessage(message) {
const msg = JSON.parse(JSON.stringify(message)); // Clone!
msg.id = this._nextId++;
if (DEBUG)
console.log('[sent]', JSON.stringify(msg));
const responsePromise = new Promise((resolve, reject) => {
this._commandResponsePromises.set(msg.id, { resolve, reject });
});
return new Promise(
(resolve) => this._socket.write(formatWSFrame(msg), resolve))
.then(() => responsePromise);
}
send(commands) {
if (Array.isArray(commands)) {
// Multiple commands means the response does not matter. There might even
// never be a response.
return Promise
.all(commands.map((command) => this._sendMessage(command)))
.then(() => {});
}
return this._sendMessage(commands);
}
waitForNotification(methodOrPredicate, description) {
const desc = description || methodOrPredicate;
const message = `Timed out waiting for matching notification (${desc})`;
return fires(
this._asyncWaitForNotification(methodOrPredicate), message, TIMEOUT);
}
async _asyncWaitForNotification(methodOrPredicate) {
function matchMethod(notification) {
return notification.method === methodOrPredicate;
}
const predicate =
typeof methodOrPredicate === 'string' ? matchMethod : methodOrPredicate;
let notification = null;
do {
if (this._unprocessedNotifications.length) {
notification = this._unprocessedNotifications.shift();
} else {
notification = await new Promise(
(resolve) => this._notificationCallback = resolve);
}
} while (!predicate(notification));
return notification;
}
_isBreakOnLineNotification(message, line, expectedScriptPath) {
if (message.method === 'Debugger.paused') {
const callFrame = message.params.callFrames[0];
const location = callFrame.location;
const scriptPath = this._scriptsIdsByUrl.get(location.scriptId);
assert.strictEqual(decodeURIComponent(scriptPath),
decodeURIComponent(expectedScriptPath),
`${scriptPath} !== ${expectedScriptPath}`);
assert.strictEqual(location.lineNumber, line);
return true;
}
}
waitForBreakOnLine(line, url) {
return this
.waitForNotification(
(notification) =>
this._isBreakOnLineNotification(notification, line, url),
`break on ${url}:${line}`);
}
waitForPauseOnStart() {
return this
.waitForNotification(
(notification) =>
notification.method === 'Debugger.paused' && notification.params.reason === 'Break on start',
'break on start');
}
pausedDetails() {
return this._pausedDetails;
}
_matchesConsoleOutputNotification(notification, type, values) {
if (!Array.isArray(values))
values = [ values ];
if (notification.method === 'Runtime.consoleAPICalled') {
const params = notification.params;
if (params.type === type) {
let i = 0;
for (const value of params.args) {
if (value.value !== values[i++])
return false;
}
return i === values.length;
}
}
}
waitForConsoleOutput(type, values) {
const desc = `Console output matching ${JSON.stringify(values)}`;
return this.waitForNotification(
(notification) => this._matchesConsoleOutputNotification(notification,
type, values),
desc);
}
async runToCompletion() {
console.log('[test]', 'Verify node waits for the frontend to disconnect');
await this.send({ 'method': 'Debugger.resume' });
await this.waitForNotification((notification) => {
if (notification.method === 'Debugger.paused') {
this.send({ 'method': 'Debugger.resume' });
}
return notification.method === 'Runtime.executionContextDestroyed' &&
notification.params.executionContextId === 1;
});
await this.waitForDisconnect();
}
async waitForDisconnect() {
while ((await this._instance.nextStderrString()) !==
'Waiting for the debugger to disconnect...');
await this.disconnect();
}
scriptPath() {
return this._instance.scriptPath();
}
script() {
return this._instance.script();
}
scriptURL() {
return pathToFileURL(this.scriptPath());
}
}
class NodeInstance extends EventEmitter {
constructor(inspectorFlags = ['--inspect-brk=0', '--expose-internals'],
scriptContents = '',
scriptFile = _MAINSCRIPT,
logger = console) {
super();
this._logger = logger;
this._scriptPath = scriptFile;
this._script = scriptFile ? null : scriptContents;
this._portCallback = null;
this.resetPort();
this._process = spawnChildProcess(inspectorFlags, scriptContents,
scriptFile);
this._running = true;
this._stderrLineCallback = null;
this._unprocessedStderrLines = [];
this._process.stdout.on('data', makeBufferingDataCallback(
(line) => {
this.emit('stdout', line);
this._logger.log('[out]', line);
}));
this._process.stderr.on('data', makeBufferingDataCallback(
(message) => this.onStderrLine(message)));
this._shutdownPromise = new Promise((resolve) => {
this._process.once('exit', (exitCode, signal) => {
if (signal) {
this._logger.error(`[err] child process crashed, signal ${signal}`);
}
resolve({ exitCode, signal });
this._running = false;
});
});
}
get pid() {
return this._process.pid;
}
resetPort() {
this.portPromise = new Promise((resolve) => this._portCallback = resolve);
}
static async startViaSignal(scriptContents) {
const instance = new NodeInstance(
['--expose-internals', '--inspect-port=0'],
`${scriptContents}\nprocess._rawDebug('started');`, undefined);
const msg = 'Timed out waiting for process to start';
while (await fires(instance.nextStderrString(), msg, TIMEOUT) !== 'started');
process._debugProcess(instance._process.pid);
return instance;
}
onStderrLine(line) {
this.emit('stderr', line);
this._logger.log('[err]', line);
if (this._portCallback) {
const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/);
if (matches) {
this._portCallback(matches[1]);
this._portCallback = null;
}
}
if (this._stderrLineCallback) {
this._stderrLineCallback(line);
this._stderrLineCallback = null;
} else {
this._unprocessedStderrLines.push(line);
}
}
httpGet(host, path, hostHeaderValue) {
this._logger.log('[test]', `Testing ${path}`);
const headers = hostHeaderValue ? { 'Host': hostHeaderValue } : null;
return this.portPromise.then((port) => new Promise((resolve, reject) => {
const req = http.get({ host, port, family: 4, path, headers }, (res) => {
let response = '';
res.setEncoding('utf8');
res
.on('data', (data) => response += data.toString())
.on('end', () => {
resolve(response);
});
});
req.on('error', reject);
})).then((response) => {
try {
return JSON.parse(response);
} catch (e) {
e.body = response;
throw e;
}
});
}
async sendUpgradeRequest() {
const response = await this.httpGet(null, '/json/list');
const devtoolsUrl = response[0].webSocketDebuggerUrl;
const port = await this.portPromise;
return http.get({
port,
family: 4,
path: new URL(devtoolsUrl).pathname,
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': 13,
'Sec-WebSocket-Key': 'key==',
},
});
}
async connectInspectorSession() {
this._logger.log('[test]', 'Connecting to a child Node process');
const upgradeRequest = await this.sendUpgradeRequest();
return new Promise((resolve) => {
upgradeRequest
.on('upgrade',
(message, socket) => resolve(new InspectorSession(socket, this)))
.on('response', common.mustNotCall('Upgrade was not received'));
});
}
async expectConnectionDeclined() {
this._logger.log('[test]', 'Checking upgrade is not possible');
const upgradeRequest = await this.sendUpgradeRequest();
return new Promise((resolve) => {
upgradeRequest
.on('upgrade', common.mustNotCall('Upgrade was received'))
.on('response', (response) =>
response.on('data', () => {})
.on('end', () => resolve(response.statusCode)));
});
}
expectShutdown() {
return this._shutdownPromise;
}
nextStderrString() {
if (this._unprocessedStderrLines.length)
return Promise.resolve(this._unprocessedStderrLines.shift());
return new Promise((resolve) => this._stderrLineCallback = resolve);
}
write(message) {
this._process.stdin.write(message);
}
kill() {
this._process.kill();
return this.expectShutdown();
}
scriptPath() {
return this._scriptPath;
}
script() {
if (this._script === null)
this._script = fs.readFileSync(this.scriptPath(), 'utf8');
return this._script;
}
}
function onResolvedOrRejected(promise, callback) {
return promise.then((result) => {
callback();
return result;
}, (error) => {
callback();
throw error;
});
}
function timeoutPromise(error, timeoutMs) {
let clearCallback = null;
let done = false;
const promise = onResolvedOrRejected(new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(error), timeoutMs);
clearCallback = () => {
if (done)
return;
clearTimeout(timeout);
resolve();
};
}), () => done = true);
promise.clear = clearCallback;
return promise;
}
// Returns a new promise that will propagate `promise` resolution or rejection
// if that happens within the `timeoutMs` timespan, or rejects with `error` as
// a reason otherwise.
function fires(promise, error, timeoutMs) {
const timeout = timeoutPromise(error, timeoutMs);
return Promise.race([
onResolvedOrRejected(promise, () => timeout.clear()),
timeout,
]);
}
module.exports = {
NodeInstance,
};

View File

@@ -0,0 +1,58 @@
'use strict';
// Utilities for internet-related tests
const addresses = {
// A generic host that has registered common DNS records,
// supports both IPv4 and IPv6, and provides basic HTTP/HTTPS services
INET_HOST: 'nodejs.org',
// A host that provides IPv4 services
INET4_HOST: 'nodejs.org',
// A host that provides IPv6 services
INET6_HOST: 'nodejs.org',
// An accessible IPv4 IP,
// defaults to the Google Public DNS IPv4 address
INET4_IP: '8.8.8.8',
// An accessible IPv6 IP,
// defaults to the Google Public DNS IPv6 address
INET6_IP: '2001:4860:4860::8888',
// An invalid host that cannot be resolved
// See https://tools.ietf.org/html/rfc2606#section-2
INVALID_HOST: 'something.invalid',
// A host with MX records registered
MX_HOST: 'nodejs.org',
// On some systems, .invalid returns a server failure/try again rather than
// record not found. Use this to guarantee record not found.
NOT_FOUND: 'come.on.fhqwhgads.test',
// A host with SRV records registered
SRV_HOST: '_caldav._tcp.google.com',
// A host with PTR records registered
PTR_HOST: '8.8.8.8.in-addr.arpa',
// A host with NAPTR records registered
NAPTR_HOST: 'sip2sip.info',
// A host with SOA records registered
SOA_HOST: 'nodejs.org',
// A host with CAA record registered
CAA_HOST: 'google.com',
// A host with CNAME records registered
CNAME_HOST: 'blog.nodejs.org',
// A host with NS records registered
NS_HOST: 'nodejs.org',
// A host with TXT records registered
TXT_HOST: 'nodejs.org',
// An accessible IPv4 DNS server
DNS4_SERVER: '8.8.8.8',
// An accessible IPv4 DNS server
DNS6_SERVER: '2001:4860:4860::8888',
};
for (const key of Object.keys(addresses)) {
const envName = `NODE_TEST_${key}`;
if (process.env[envName]) {
addresses[key] = process.env[envName];
}
}
module.exports = {
addresses,
};

View File

@@ -0,0 +1,57 @@
'use strict';
const assert = require('assert');
const common = require('./');
// The formats could change when V8 is updated, then the tests should be
// updated accordingly.
function assertResultShape(result) {
assert.strictEqual(typeof result.jsMemoryEstimate, 'number');
assert.strictEqual(typeof result.jsMemoryRange[0], 'number');
assert.strictEqual(typeof result.jsMemoryRange[1], 'number');
}
function assertSummaryShape(result) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof result.total, 'object');
assertResultShape(result.total);
}
function assertDetailedShape(result, contexts = 0) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof result.total, 'object');
assert.strictEqual(typeof result.current, 'object');
assertResultShape(result.total);
assertResultShape(result.current);
if (contexts === 0) {
assert.deepStrictEqual(result.other, []);
} else {
assert.strictEqual(result.other.length, contexts);
for (const item of result.other) {
assertResultShape(item);
}
}
}
function assertSingleDetailedShape(result) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof result.total, 'object');
assert.strictEqual(typeof result.current, 'object');
assert.deepStrictEqual(result.other, []);
assertResultShape(result.total);
assertResultShape(result.current);
}
function expectExperimentalWarning() {
common.expectWarning(
'ExperimentalWarning',
'vm.measureMemory is an experimental feature and might change at any time',
);
}
module.exports = {
assertSummaryShape,
assertDetailedShape,
assertSingleDetailedShape,
expectExperimentalWarning,
};

View File

@@ -0,0 +1,23 @@
'use strict';
const net = require('net');
const options = { port: 0, reusePort: true };
function checkSupportReusePort() {
return new Promise((resolve, reject) => {
const server = net.createServer().listen(options);
server.on('listening', () => {
server.close(resolve);
});
server.on('error', (err) => {
console.log('The `reusePort` option is not supported:', err.message);
server.close();
reject(err);
});
});
}
module.exports = {
checkSupportReusePort,
options,
};

View File

@@ -0,0 +1,3 @@
{
"type": "commonjs"
}

View File

@@ -0,0 +1,138 @@
'use strict';
const assert = require('assert');
function getTestCases(isWorker = false) {
const cases = [];
function exitsOnExitCodeSet() {
process.exitCode = 42;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 42);
assert.strictEqual(code, 42);
});
}
cases.push({ func: exitsOnExitCodeSet, result: 42 });
function changesCodeViaExit() {
process.exitCode = 99;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 42);
assert.strictEqual(code, 42);
});
process.exit(42);
}
cases.push({ func: changesCodeViaExit, result: 42 });
function changesCodeZeroExit() {
process.exitCode = 99;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 0);
assert.strictEqual(code, 0);
});
process.exit(0);
}
cases.push({ func: changesCodeZeroExit, result: 0 });
function exitWithOneOnUncaught() {
process.exitCode = 99;
process.on('exit', (code) => {
// Cannot use assert because it will be uncaughtException -> 1 exit code
// that will render this test useless
if (code !== 1 || process.exitCode !== 1) {
console.log('wrong code! expected 1 for uncaughtException');
process.exit(99);
}
});
throw new Error('ok');
}
cases.push({
func: exitWithOneOnUncaught,
result: 1,
error: /^Error: ok$/,
});
function changeCodeInsideExit() {
process.exitCode = 95;
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 95);
assert.strictEqual(code, 95);
process.exitCode = 99;
});
}
cases.push({ func: changeCodeInsideExit, result: 99 });
function zeroExitWithUncaughtHandler() {
const noop = () => { };
process.on('exit', (code) => {
process.off('uncaughtException', noop);
assert.strictEqual(process.exitCode, undefined);
assert.strictEqual(code, 0);
});
process.on('uncaughtException', noop);
throw new Error('ok');
}
cases.push({ func: zeroExitWithUncaughtHandler, result: 0 });
function changeCodeInUncaughtHandler() {
const modifyExitCode = () => { process.exitCode = 97; };
process.on('exit', (code) => {
process.off('uncaughtException', modifyExitCode);
assert.strictEqual(process.exitCode, 97);
assert.strictEqual(code, 97);
});
process.on('uncaughtException', modifyExitCode);
throw new Error('ok');
}
cases.push({ func: changeCodeInUncaughtHandler, result: 97 });
function changeCodeInExitWithUncaught() {
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 1);
assert.strictEqual(code, 1);
process.exitCode = 98;
});
throw new Error('ok');
}
cases.push({
func: changeCodeInExitWithUncaught,
result: 98,
error: /^Error: ok$/,
});
function exitWithZeroInExitWithUncaught() {
process.on('exit', (code) => {
assert.strictEqual(process.exitCode, 1);
assert.strictEqual(code, 1);
process.exitCode = 0;
});
throw new Error('ok');
}
cases.push({
func: exitWithZeroInExitWithUncaught,
result: 0,
error: /^Error: ok$/,
});
function exitWithThrowInUncaughtHandler() {
process.on('uncaughtException', () => {
throw new Error('ok');
});
throw new Error('bad');
}
cases.push({
func: exitWithThrowInUncaughtHandler,
result: isWorker ? 1 : 7,
error: /^Error: ok$/,
});
function exitWithUndefinedFatalException() {
process._fatalException = undefined;
throw new Error('ok');
}
cases.push({
func: exitWithUndefinedFatalException,
result: 6,
});
return cases;
}
exports.getTestCases = getTestCases;

View File

@@ -0,0 +1,67 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function getHeapProfiles(dir) {
const list = fs.readdirSync(dir);
return list
.filter((file) => file.endsWith('.heapprofile'))
.map((file) => path.join(dir, file));
}
function findFirstFrameInNode(root, func) {
const first = root.children.find(
(child) => child.callFrame.functionName === func,
);
if (first) {
return first;
}
for (const child of root.children) {
const first = findFirstFrameInNode(child, func);
if (first) {
return first;
}
}
return undefined;
}
function findFirstFrame(file, func) {
const data = fs.readFileSync(file, 'utf8');
const profile = JSON.parse(data);
const first = findFirstFrameInNode(profile.head, func);
return { frame: first, roots: profile.head.children };
}
function verifyFrames(output, file, func) {
const { frame, roots } = findFirstFrame(file, func);
if (!frame) {
// Show native debug output and the profile for debugging.
console.log(output.stderr.toString());
console.log(roots);
}
assert.notStrictEqual(frame, undefined);
}
// We need to set --heap-prof-interval to a small enough value to make
// sure we can find our workload in the samples, so we need to set
// TEST_ALLOCATION > kHeapProfInterval.
const kHeapProfInterval = 128;
const TEST_ALLOCATION = kHeapProfInterval * 2;
const env = {
...process.env,
TEST_ALLOCATION,
NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER',
};
// TODO(joyeecheung): share the fixutres with v8 coverage tests
module.exports = {
getHeapProfiles,
verifyFrames,
findFirstFrame,
kHeapProfInterval,
TEST_ALLOCATION,
env,
};

View File

@@ -0,0 +1,346 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const net = require('net');
const os = require('os');
const path = require('path');
const util = require('util');
const cpus = os.cpus();
function findReports(pid, dir) {
// Default filenames are of the form
// report.<date>.<time>.<pid>.<tid>.<seq>.json
const format = '^report\\.\\d+\\.\\d+\\.' + pid + '\\.\\d+\\.\\d+\\.json$';
const filePattern = new RegExp(format);
const files = fs.readdirSync(dir);
const results = [];
files.forEach((file) => {
if (filePattern.test(file))
results.push(path.join(dir, file));
});
return results;
}
function validate(filepath, fields) {
const report = fs.readFileSync(filepath, 'utf8');
if (process.report.compact) {
const end = report.indexOf('\n');
assert.strictEqual(end, report.length - 1);
}
validateContent(JSON.parse(report), fields);
}
function validateContent(report, fields = []) {
if (typeof report === 'string') {
try {
report = JSON.parse(report);
} catch {
throw new TypeError(
'validateContent() expects a JSON string or JavaScript Object');
}
}
try {
_validateContent(report, fields);
} catch (err) {
try {
err.stack += util.format('\n------\nFailing Report:\n%O', report);
} catch {
// Continue regardless of error.
}
throw err;
}
}
function _validateContent(report, fields = []) {
const isWindows = process.platform === 'win32';
const isJavaScriptThreadReport = report.javascriptHeap != null;
// Verify that all sections are present as own properties of the report.
const sections = ['header', 'nativeStack', 'javascriptStack', 'libuv',
'sharedObjects', 'resourceUsage', 'workers'];
if (!process.report.excludeEnv) {
sections.push('environmentVariables');
}
if (!isWindows)
sections.push('userLimits');
if (report.uvthreadResourceUsage)
sections.push('uvthreadResourceUsage');
if (isJavaScriptThreadReport)
sections.push('javascriptHeap');
checkForUnknownFields(report, sections);
sections.forEach((section) => {
assert(Object.hasOwn(report, section));
assert(typeof report[section] === 'object' && report[section] !== null);
});
fields.forEach((field) => {
function checkLoop(actual, rest, expect) {
actual = actual[rest.shift()];
if (rest.length === 0 && actual !== undefined) {
assert.strictEqual(actual, expect);
} else {
assert(actual);
checkLoop(actual, rest, expect);
}
}
let actual, expect;
if (Array.isArray(field)) {
[actual, expect] = field;
} else {
actual = field;
expect = undefined;
}
checkLoop(report, actual.split('.'), expect);
});
// Verify the format of the header section.
const header = report.header;
const headerFields = ['event', 'trigger', 'filename', 'dumpEventTime',
'dumpEventTimeStamp', 'processId', 'commandLine',
'nodejsVersion', 'wordSize', 'arch', 'platform',
'componentVersions', 'release', 'osName', 'osRelease',
'osVersion', 'osMachine', 'cpus', 'host',
'glibcVersionRuntime', 'glibcVersionCompiler', 'cwd',
'reportVersion', 'networkInterfaces', 'threadId'];
checkForUnknownFields(header, headerFields);
assert.strictEqual(header.reportVersion, 4); // Increment as needed.
assert.strictEqual(typeof header.event, 'string');
assert.strictEqual(typeof header.trigger, 'string');
assert(typeof header.filename === 'string' || header.filename === null);
assert.notStrictEqual(new Date(header.dumpEventTime).toString(),
'Invalid Date');
assert(String(+header.dumpEventTimeStamp), header.dumpEventTimeStamp);
assert(Number.isSafeInteger(header.processId));
assert(Number.isSafeInteger(header.threadId) || header.threadId === null);
assert.strictEqual(typeof header.cwd, 'string');
assert(Array.isArray(header.commandLine));
header.commandLine.forEach((arg) => {
assert.strictEqual(typeof arg, 'string');
});
assert.strictEqual(header.nodejsVersion, process.version);
assert(Number.isSafeInteger(header.wordSize));
assert.strictEqual(header.arch, os.arch());
assert.strictEqual(header.platform, os.platform());
assert.deepStrictEqual(header.componentVersions, process.versions);
assert.deepStrictEqual(header.release, process.release);
assert.strictEqual(header.osName, os.type());
assert.strictEqual(header.osRelease, os.release());
assert.strictEqual(typeof header.osVersion, 'string');
assert.strictEqual(typeof header.osMachine, 'string');
assert(Array.isArray(header.cpus));
assert.strictEqual(header.cpus.length, cpus.length);
header.cpus.forEach((cpu) => {
assert.strictEqual(typeof cpu.model, 'string');
assert.strictEqual(typeof cpu.speed, 'number');
assert.strictEqual(typeof cpu.user, 'number');
assert.strictEqual(typeof cpu.nice, 'number');
assert.strictEqual(typeof cpu.sys, 'number');
assert.strictEqual(typeof cpu.idle, 'number');
assert.strictEqual(typeof cpu.irq, 'number');
assert(cpus.some((c) => {
return c.model === cpu.model;
}));
});
assert(Array.isArray(header.networkInterfaces));
header.networkInterfaces.forEach((iface) => {
assert.strictEqual(typeof iface.name, 'string');
assert.strictEqual(typeof iface.internal, 'boolean');
assert.match(iface.mac, /^([0-9A-F][0-9A-F]:){5}[0-9A-F]{2}$/i);
if (iface.family === 'IPv4') {
assert.strictEqual(net.isIPv4(iface.address), true);
assert.strictEqual(net.isIPv4(iface.netmask), true);
assert.strictEqual(iface.scopeid, undefined);
} else if (iface.family === 'IPv6') {
assert.strictEqual(net.isIPv6(iface.address), true);
assert.strictEqual(net.isIPv6(iface.netmask), true);
assert(Number.isInteger(iface.scopeid));
} else {
assert.strictEqual(iface.family, 'unknown');
assert.strictEqual(iface.address, undefined);
assert.strictEqual(iface.netmask, undefined);
assert.strictEqual(iface.scopeid, undefined);
}
});
assert.strictEqual(header.host, os.hostname());
// Verify the format of the nativeStack section.
assert(Array.isArray(report.nativeStack));
report.nativeStack.forEach((frame) => {
assert(typeof frame === 'object' && frame !== null);
checkForUnknownFields(frame, ['pc', 'symbol']);
assert.strictEqual(typeof frame.pc, 'string');
assert.match(frame.pc, /^0x[0-9a-f]+$/);
assert.strictEqual(typeof frame.symbol, 'string');
});
if (isJavaScriptThreadReport) {
// Verify the format of the javascriptStack section.
checkForUnknownFields(report.javascriptStack,
['message', 'stack', 'errorProperties']);
assert.strictEqual(typeof report.javascriptStack.errorProperties,
'object');
assert.strictEqual(typeof report.javascriptStack.message, 'string');
if (report.javascriptStack.stack !== undefined) {
assert(Array.isArray(report.javascriptStack.stack));
report.javascriptStack.stack.forEach((frame) => {
assert.strictEqual(typeof frame, 'string');
});
}
// Verify the format of the javascriptHeap section.
const heap = report.javascriptHeap;
// See `PrintGCStatistics` in node_report.cc
const jsHeapFields = [
'totalMemory',
'executableMemory',
'totalCommittedMemory',
'availableMemory',
'totalGlobalHandlesMemory',
'usedGlobalHandlesMemory',
'usedMemory',
'memoryLimit',
'mallocedMemory',
'externalMemory',
'peakMallocedMemory',
'nativeContextCount',
'detachedContextCount',
'doesZapGarbage',
'heapSpaces',
];
checkForUnknownFields(heap, jsHeapFields);
// Do not check `heapSpaces` here
for (let i = 0; i < jsHeapFields.length - 1; i++) {
assert(
Number.isSafeInteger(heap[jsHeapFields[i]]),
`heap.${jsHeapFields[i]} is not a safe integer`,
);
}
assert(typeof heap.heapSpaces === 'object' && heap.heapSpaces !== null);
const heapSpaceFields = ['memorySize', 'committedMemory', 'capacity',
'used', 'available'];
Object.keys(heap.heapSpaces).forEach((spaceName) => {
const space = heap.heapSpaces[spaceName];
checkForUnknownFields(space, heapSpaceFields);
heapSpaceFields.forEach((field) => {
assert(Number.isSafeInteger(space[field]));
});
});
}
// Verify the format of the resourceUsage section.
const usage = { ...report.resourceUsage };
// Delete it, otherwise checkForUnknownFields will throw error
delete usage.constrained_memory;
const resourceUsageFields = ['userCpuSeconds', 'kernelCpuSeconds',
'cpuConsumptionPercent', 'userCpuConsumptionPercent',
'kernelCpuConsumptionPercent',
'maxRss', 'rss', 'free_memory', 'total_memory',
'available_memory', 'pageFaults', 'fsActivity'];
checkForUnknownFields(usage, resourceUsageFields);
assert.strictEqual(typeof usage.userCpuSeconds, 'number');
assert.strictEqual(typeof usage.kernelCpuSeconds, 'number');
assert.strictEqual(typeof usage.cpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.userCpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.kernelCpuConsumptionPercent, 'number');
assert(typeof usage.rss, 'string');
assert(typeof usage.maxRss, 'string');
assert(typeof usage.free_memory, 'string');
assert(typeof usage.total_memory, 'string');
assert(typeof usage.available_memory, 'string');
// This field may not exist
if (report.resourceUsage.constrained_memory) {
assert(typeof report.resourceUsage.constrained_memory, 'string');
}
assert(typeof usage.pageFaults === 'object' && usage.pageFaults !== null);
checkForUnknownFields(usage.pageFaults, ['IORequired', 'IONotRequired']);
assert(Number.isSafeInteger(usage.pageFaults.IORequired));
assert(Number.isSafeInteger(usage.pageFaults.IONotRequired));
assert(typeof usage.fsActivity === 'object' && usage.fsActivity !== null);
checkForUnknownFields(usage.fsActivity, ['reads', 'writes']);
assert(Number.isSafeInteger(usage.fsActivity.reads));
assert(Number.isSafeInteger(usage.fsActivity.writes));
// Verify the format of the uvthreadResourceUsage section, if present.
if (report.uvthreadResourceUsage) {
const usage = report.uvthreadResourceUsage;
const threadUsageFields = ['userCpuSeconds', 'kernelCpuSeconds',
'cpuConsumptionPercent', 'fsActivity',
'userCpuConsumptionPercent',
'kernelCpuConsumptionPercent'];
checkForUnknownFields(usage, threadUsageFields);
assert.strictEqual(typeof usage.userCpuSeconds, 'number');
assert.strictEqual(typeof usage.kernelCpuSeconds, 'number');
assert.strictEqual(typeof usage.cpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.userCpuConsumptionPercent, 'number');
assert.strictEqual(typeof usage.kernelCpuConsumptionPercent, 'number');
assert(typeof usage.fsActivity === 'object' && usage.fsActivity !== null);
checkForUnknownFields(usage.fsActivity, ['reads', 'writes']);
assert(Number.isSafeInteger(usage.fsActivity.reads));
assert(Number.isSafeInteger(usage.fsActivity.writes));
}
// Verify the format of the libuv section.
assert(Array.isArray(report.libuv));
report.libuv.forEach((resource) => {
assert.strictEqual(typeof resource.type, 'string');
assert.strictEqual(typeof resource.address, 'string');
assert.match(resource.address, /^0x[0-9a-f]+$/);
assert.strictEqual(typeof resource.is_active, 'boolean');
assert.strictEqual(typeof resource.is_referenced,
resource.type === 'loop' ? 'undefined' : 'boolean');
});
if (!process.report.excludeEnv) {
// Verify the format of the environmentVariables section.
for (const [key, value] of Object.entries(report.environmentVariables)) {
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof value, 'string');
}
}
// Verify the format of the userLimits section on non-Windows platforms.
if (!isWindows) {
const userLimitsFields = ['core_file_size_blocks', 'data_seg_size_kbytes',
'file_size_blocks', 'max_locked_memory_bytes',
'max_memory_size_kbytes', 'open_files',
'stack_size_bytes', 'cpu_time_seconds',
'max_user_processes', 'virtual_memory_kbytes'];
checkForUnknownFields(report.userLimits, userLimitsFields);
for (const [type, limits] of Object.entries(report.userLimits)) {
assert.strictEqual(typeof type, 'string');
assert(typeof limits === 'object' && limits !== null);
checkForUnknownFields(limits, ['soft', 'hard']);
assert(typeof limits.soft === 'number' || limits.soft === 'unlimited',
`Invalid ${type} soft limit of ${limits.soft}`);
assert(typeof limits.hard === 'number' || limits.hard === 'unlimited',
`Invalid ${type} hard limit of ${limits.hard}`);
}
}
// Verify the format of the sharedObjects section.
assert(Array.isArray(report.sharedObjects));
report.sharedObjects.forEach((sharedObject) => {
assert.strictEqual(typeof sharedObject, 'string');
});
// Verify the format of the workers section.
assert(Array.isArray(report.workers));
report.workers.forEach((worker) => _validateContent(worker));
}
function checkForUnknownFields(actual, expected) {
Object.keys(actual).forEach((field) => {
assert(expected.includes(field), `'${field}' not expected in ${expected}`);
});
}
module.exports = { findReports, validate, validateContent };

View File

@@ -0,0 +1,27 @@
'use strict';
if (require.main !== module) {
const { spawnSync } = require('child_process');
function runModuleAs(filename, flags, spawnOptions, role) {
return spawnSync(process.execPath,
[...flags, __filename, role, filename], spawnOptions);
}
module.exports = runModuleAs;
return;
}
const { Worker, isMainThread, workerData } = require('worker_threads');
if (isMainThread) {
if (process.argv[2] === 'worker') {
new Worker(__filename, {
workerData: process.argv[3],
});
return;
}
require(process.argv[3]);
} else {
require(workerData);
}

View File

@@ -0,0 +1,144 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { inspect } = require('util');
const { readFileSync, copyFileSync, statSync } = require('fs');
const {
spawnSyncAndExitWithoutError,
} = require('../common/child_process');
function skipIfSingleExecutableIsNotSupported() {
if (!process.config.variables.single_executable_application)
common.skip('Single Executable Application support has been disabled.');
if (!['darwin', 'win32', 'linux'].includes(process.platform))
common.skip(`Unsupported platform ${process.platform}.`);
if (process.platform === 'linux' && process.config.variables.is_debug === 1)
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');
if (process.config.variables.node_shared)
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
'libnode.so.112: cannot open shared object file: No such file or directory`.');
if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');
if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');
if (process.config.variables.want_separate_host_toolset !== 0)
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');
if (process.platform === 'linux') {
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
const isAlpine = /^NAME="Alpine Linux"/m.test(osReleaseText);
if (isAlpine) common.skip('Alpine Linux is not supported.');
if (process.arch === 's390x') {
common.skip('On s390x, postject fails with `memory access out of bounds`.');
}
}
if (process.config.variables.ubsan) {
common.skip('UndefinedBehavior Sanitizer is not supported');
}
try {
readFileSync(process.execPath);
} catch (e) {
if (e.code === 'ERR_FS_FILE_TOO_LARGE') {
common.skip('The Node.js binary is too large to be supported by postject');
}
}
tmpdir.refresh();
// The SEA tests involve making a copy of the executable and writing some fixtures
// to the tmpdir. To be safe, ensure that the disk space has at least a copy of the
// executable and some extra space for blobs and configs is available.
const stat = statSync(process.execPath);
const expectedSpace = stat.size + 10 * 1024 * 1024;
if (!tmpdir.hasEnoughSpace(expectedSpace)) {
common.skip(`Available disk space < ${Math.floor(expectedSpace / 1024 / 1024)} MB`);
}
}
function generateSEA(targetExecutable, sourceExecutable, seaBlob, verifyWorkflow = false) {
try {
copyFileSync(sourceExecutable, targetExecutable);
} catch (e) {
const message = `Cannot copy ${sourceExecutable} to ${targetExecutable}: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Copied ${sourceExecutable} to ${targetExecutable}`);
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
try {
spawnSyncAndExitWithoutError(process.execPath, [
postjectFile,
targetExecutable,
'NODE_SEA_BLOB',
seaBlob,
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
]);
} catch (e) {
const message = `Cannot inject ${seaBlob} into ${targetExecutable}: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Injected ${seaBlob} into ${targetExecutable}`);
if (process.platform === 'darwin') {
try {
spawnSyncAndExitWithoutError('codesign', [ '--sign', '-', targetExecutable ]);
spawnSyncAndExitWithoutError('codesign', [ '--verify', targetExecutable ]);
} catch (e) {
const message = `Cannot sign ${targetExecutable}: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Signed ${targetExecutable}`);
} else if (process.platform === 'win32') {
try {
spawnSyncAndExitWithoutError('where', [ 'signtool' ]);
} catch (e) {
const message = `Cannot find signtool: ${inspect(e)}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
let stderr;
try {
({ stderr } = spawnSyncAndExitWithoutError('signtool', [ 'sign', '/fd', 'SHA256', targetExecutable ]));
spawnSyncAndExitWithoutError('signtool', ['verify', '/pa', 'SHA256', targetExecutable]);
} catch (e) {
const message = `Cannot sign ${targetExecutable}: ${inspect(e)}\n${stderr}`;
if (verifyWorkflow) {
throw new Error(message);
}
common.skip(message);
}
console.log(`Signed ${targetExecutable}`);
}
}
module.exports = {
skipIfSingleExecutableIsNotSupported,
generateSEA,
};

View File

@@ -0,0 +1,50 @@
'use strict';
const common = require('../common');
const path = require('path');
const kNodeShared = Boolean(process.config.variables.node_shared);
const kShlibSuffix = process.config.variables.shlib_suffix;
const kExecPath = path.dirname(process.execPath);
// If node executable is linked to shared lib, need to take care about the
// shared lib path.
function addLibraryPath(env) {
if (!kNodeShared) {
return;
}
env ||= process.env;
env.LD_LIBRARY_PATH =
(env.LD_LIBRARY_PATH ? env.LD_LIBRARY_PATH + path.delimiter : '') +
kExecPath;
// For AIX.
env.LIBPATH =
(env.LIBPATH ? env.LIBPATH + path.delimiter : '') +
kExecPath;
// For macOS.
env.DYLD_LIBRARY_PATH =
(env.DYLD_LIBRARY_PATH ? env.DYLD_LIBRARY_PATH + path.delimiter : '') +
kExecPath;
// For Windows.
env.PATH = (env.PATH ? env.PATH + path.delimiter : '') + kExecPath;
}
// Get the full path of shared lib.
function getSharedLibPath() {
if (common.isWindows) {
return path.join(kExecPath, 'node.dll');
}
return path.join(kExecPath, `libnode.${kShlibSuffix}`);
}
// Get the binary path of stack frames.
function getBinaryPath() {
return kNodeShared ? getSharedLibPath() : process.execPath;
}
module.exports = {
addLibraryPath,
getBinaryPath,
getSharedLibPath,
};

View File

@@ -0,0 +1,65 @@
'use strict';
const tmpdir = require('../common/tmpdir');
const { spawnSync } = require('child_process');
const fs = require('fs');
const assert = require('assert');
function buildSnapshot(entry, env) {
const child = spawnSync(process.execPath, [
'--snapshot-blob',
tmpdir.resolve('snapshot.blob'),
'--build-snapshot',
entry,
], {
cwd: tmpdir.path,
env: {
...process.env,
...env,
},
});
const stderr = child.stderr.toString();
const stdout = child.stdout.toString();
console.log('[stderr]');
console.log(stderr);
console.log('[stdout]');
console.log(stdout);
assert.strictEqual(child.status, 0);
const stats = fs.statSync(tmpdir.resolve('snapshot.blob'));
assert(stats.isFile());
return { child, stderr, stdout };
}
function runWithSnapshot(entry, env) {
const args = ['--snapshot-blob', tmpdir.resolve('snapshot.blob')];
if (entry !== undefined) {
args.push(entry);
}
const child = spawnSync(process.execPath, args, {
cwd: tmpdir.path,
env: {
...process.env,
...env,
},
});
const stderr = child.stderr.toString();
const stdout = child.stdout.toString();
console.log('[stderr]');
console.log(stderr);
console.log('[stdout]');
console.log(stdout);
assert.strictEqual(child.status, 0);
return { child, stderr, stdout };
}
module.exports = {
buildSnapshot,
runWithSnapshot,
};

View File

@@ -0,0 +1,12 @@
'use strict';
module.exports = function tick(x, cb) {
function ontick() {
if (--x === 0) {
if (typeof cb === 'function') cb();
} else {
setImmediate(ontick);
}
}
setImmediate(ontick);
};

View File

@@ -0,0 +1,176 @@
/* eslint-disable node-core/crypto-check */
'use strict';
const crypto = require('crypto');
const net = require('net');
exports.ccs = Buffer.from('140303000101', 'hex');
class TestTLSSocket extends net.Socket {
constructor(server_cert) {
super();
this.server_cert = server_cert;
this.version = Buffer.from('0303', 'hex');
this.handshake_list = [];
// AES128-GCM-SHA256
this.ciphers = Buffer.from('000002009c0', 'hex');
this.pre_primary_secret =
Buffer.concat([this.version, crypto.randomBytes(46)]);
this.primary_secret = null;
this.write_seq = 0;
this.client_random = crypto.randomBytes(32);
this.on('handshake', (msg) => {
this.handshake_list.push(msg);
});
this.on('server_random', (server_random) => {
this.primary_secret = PRF12('sha256', this.pre_primary_secret,
'primary secret',
Buffer.concat([this.client_random,
server_random]),
48);
const key_block = PRF12('sha256', this.primary_secret,
'key expansion',
Buffer.concat([server_random,
this.client_random]),
40);
this.client_writeKey = key_block.slice(0, 16);
this.client_writeIV = key_block.slice(32, 36);
});
}
createClientHello() {
const compressions = Buffer.from('0100', 'hex'); // null
const msg = addHandshakeHeader(0x01, Buffer.concat([
this.version, this.client_random, this.ciphers, compressions,
]));
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}
createClientKeyExchange() {
const encrypted_pre_primary_secret = crypto.publicEncrypt({
key: this.server_cert,
padding: crypto.constants.RSA_PKCS1_PADDING,
}, this.pre_primary_secret);
const length = Buffer.alloc(2);
length.writeUIntBE(encrypted_pre_primary_secret.length, 0, 2);
const msg = addHandshakeHeader(0x10, Buffer.concat([
length, encrypted_pre_primary_secret]));
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}
createFinished() {
const shasum = crypto.createHash('sha256');
shasum.update(Buffer.concat(this.handshake_list));
const message_hash = shasum.digest();
const r = PRF12('sha256', this.primary_secret,
'client finished', message_hash, 12);
const msg = addHandshakeHeader(0x14, r);
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}
createIllegalHandshake() {
const illegal_handshake = Buffer.alloc(5);
return addRecordHeader(0x16, illegal_handshake);
}
parseTLSFrame(buf) {
let offset = 0;
const record = buf.slice(offset, 5);
const type = record[0];
const length = record.slice(3, 5).readUInt16BE(0);
offset += 5;
let remaining = buf.slice(offset, offset + length);
if (type === 0x16) {
do {
remaining = this.parseTLSHandshake(remaining);
} while (remaining.length > 0);
}
offset += length;
return buf.slice(offset);
}
parseTLSHandshake(buf) {
let offset = 0;
const handshake_type = buf[offset];
if (handshake_type === 0x02) {
const server_random = buf.slice(6, 6 + 32);
this.emit('server_random', server_random);
}
offset += 1;
const length = buf.readUIntBE(offset, 3);
offset += 3;
const handshake = buf.slice(0, offset + length);
this.emit('handshake', handshake);
offset += length;
const remaining = buf.slice(offset);
return remaining;
}
encrypt(plain) {
const type = plain.slice(0, 1);
const version = plain.slice(1, 3);
const nonce = crypto.randomBytes(8);
const iv = Buffer.concat([this.client_writeIV.slice(0, 4), nonce]);
const bob = crypto.createCipheriv('aes-128-gcm', this.client_writeKey, iv);
const write_seq = Buffer.alloc(8);
write_seq.writeUInt32BE(this.write_seq++, 4);
const aad = Buffer.concat([write_seq, plain.slice(0, 5)]);
bob.setAAD(aad);
const encrypted1 = bob.update(plain.slice(5));
const encrypted = Buffer.concat([encrypted1, bob.final()]);
const tag = bob.getAuthTag();
const length = Buffer.alloc(2);
length.writeUInt16BE(nonce.length + encrypted.length + tag.length, 0);
return Buffer.concat([type, version, length, nonce, encrypted, tag]);
}
}
function addRecordHeader(type, frame) {
const record_layer = Buffer.from('0003030000', 'hex');
record_layer[0] = type;
record_layer.writeUInt16BE(frame.length, 3);
return Buffer.concat([record_layer, frame]);
}
function addHandshakeHeader(type, msg) {
const handshake_header = Buffer.alloc(4);
handshake_header[0] = type;
handshake_header.writeUIntBE(msg.length, 1, 3);
return Buffer.concat([handshake_header, msg]);
}
function PRF12(algo, secret, label, seed, size) {
const newSeed = Buffer.concat([Buffer.from(label, 'utf8'), seed]);
return P_hash(algo, secret, newSeed, size);
}
function P_hash(algo, secret, seed, size) {
const result = Buffer.alloc(size);
let hmac = crypto.createHmac(algo, secret);
hmac.update(seed);
let a = hmac.digest();
let j = 0;
while (j < size) {
hmac = crypto.createHmac(algo, secret);
hmac.update(a);
hmac.update(seed);
const b = hmac.digest();
let todo = b.length;
if (j + todo > size) {
todo = size - j;
}
b.copy(result, j, 0, todo);
j += todo;
hmac = crypto.createHmac(algo, secret);
hmac.update(a);
a = hmac.digest();
}
return result;
}
exports.TestTLSSocket = TestTLSSocket;

View File

@@ -0,0 +1,95 @@
'use strict';
const { spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const { isMainThread } = require('worker_threads');
function rmSync(pathname, useSpawn) {
if (useSpawn) {
const escapedPath = pathname.replaceAll('\\', '\\\\');
spawnSync(
process.execPath,
[
'-e',
`require("fs").rmSync("${escapedPath}", { maxRetries: 3, recursive: true, force: true });`,
],
);
} else {
fs.rmSync(pathname, { maxRetries: 3, recursive: true, force: true });
}
}
const testRoot = process.env.NODE_TEST_DIR ?
fs.realpathSync(process.env.NODE_TEST_DIR) : path.resolve(__dirname, '..');
// Using a `.` prefixed name, which is the convention for "hidden" on POSIX,
// gets tools to ignore it by default or by simple rules, especially eslint.
const tmpdirName = '.tmp.' +
(process.env.TEST_SERIAL_ID || process.env.TEST_THREAD_ID || '0');
const tmpPath = path.join(testRoot, tmpdirName);
let firstRefresh = true;
function refresh(useSpawn = false) {
rmSync(tmpPath, useSpawn);
fs.mkdirSync(tmpPath);
if (firstRefresh) {
firstRefresh = false;
// Clean only when a test uses refresh. This allows for child processes to
// use the tmpdir and only the parent will clean on exit.
process.on('exit', () => {
return onexit(useSpawn);
});
}
}
function onexit(useSpawn) {
// Change directory to avoid possible EBUSY
if (isMainThread)
process.chdir(testRoot);
try {
rmSync(tmpPath, useSpawn);
} catch (e) {
console.error('Can\'t clean tmpdir:', tmpPath);
const files = fs.readdirSync(tmpPath);
console.error('Files blocking:', files);
if (files.some((f) => f.startsWith('.nfs'))) {
// Warn about NFS "silly rename"
console.error('Note: ".nfs*" might be files that were open and ' +
'unlinked but not closed.');
console.error('See http://nfs.sourceforge.net/#faq_d2 for details.');
}
console.error();
throw e;
}
}
function resolve(...paths) {
return path.resolve(tmpPath, ...paths);
}
function hasEnoughSpace(size) {
const { bavail, bsize } = fs.statfsSync(tmpPath);
return bavail >= Math.ceil(size / bsize);
}
function fileURL(...paths) {
// When called without arguments, add explicit trailing slash
const fullPath = path.resolve(tmpPath + path.sep, ...paths);
return pathToFileURL(fullPath);
}
module.exports = {
fileURL,
hasEnoughSpace,
path: tmpPath,
refresh,
resolve,
};

View File

@@ -0,0 +1,24 @@
'use strict';
const dgram = require('dgram');
const options = { type: 'udp4', reusePort: true };
function checkSupportReusePort() {
return new Promise((resolve, reject) => {
const socket = dgram.createSocket(options);
socket.bind(0);
socket.on('listening', () => {
socket.close(resolve);
});
socket.on('error', (err) => {
console.log('The `reusePort` option is not supported:', err.message);
socket.close();
reject(err);
});
});
}
module.exports = {
checkSupportReusePort,
options,
};

View File

@@ -0,0 +1,70 @@
'use strict';
const assert = require('assert');
const { GCProfiler } = require('v8');
function collectGCProfile({ duration }) {
return new Promise((resolve) => {
const profiler = new GCProfiler();
profiler.start();
setTimeout(() => {
resolve(profiler.stop());
}, duration);
});
}
function checkGCProfile(data) {
assert.ok(data.version > 0);
assert.ok(data.startTime >= 0);
assert.ok(data.endTime >= 0);
assert.ok(Array.isArray(data.statistics));
// If the array is not empty, check it
if (data.statistics.length) {
// Just check the first one
const item = data.statistics[0];
assert.ok(typeof item.gcType === 'string');
assert.ok(item.cost >= 0);
assert.ok(typeof item.beforeGC === 'object');
assert.ok(typeof item.afterGC === 'object');
// The content of beforeGC and afterGC is same, so we just check afterGC
assert.ok(typeof item.afterGC.heapStatistics === 'object');
const heapStatisticsKeys = [
'externalMemory',
'heapSizeLimit',
'mallocedMemory',
'peakMallocedMemory',
'totalAvailableSize',
'totalGlobalHandlesSize',
'totalHeapSize',
'totalHeapSizeExecutable',
'totalPhysicalSize',
'usedGlobalHandlesSize',
'usedHeapSize',
];
heapStatisticsKeys.forEach((key) => {
assert.ok(item.afterGC.heapStatistics[key] >= 0);
});
assert.ok(typeof item.afterGC.heapSpaceStatistics === 'object');
const heapSpaceStatisticsKeys = [
'physicalSpaceSize',
'spaceAvailableSize',
'spaceName',
'spaceSize',
'spaceUsedSize',
];
heapSpaceStatisticsKeys.forEach((key) => {
const value = item.afterGC.heapSpaceStatistics[0][key];
assert.ok(key === 'spaceName' ? typeof value === 'string' : value >= 0);
});
}
}
async function testGCProfiler() {
const data = await collectGCProfile({ duration: 5000 });
checkGCProfile(data);
}
module.exports = {
collectGCProfile,
checkGCProfile,
testGCProfiler,
};

View File

@@ -0,0 +1,43 @@
// Test version set to preview1
'use strict';
const { spawnSyncAndAssert } = require('./child_process');
const fixtures = require('./fixtures');
const childPath = fixtures.path('wasi-preview-1.js');
function testWasiPreview1(args, spawnArgs = {}, expectations = {}) {
const newEnv = {
...process.env,
NODE_DEBUG_NATIVE: 'wasi',
NODE_PLATFORM: process.platform,
...spawnArgs.env,
};
spawnArgs.env = newEnv;
console.log('Testing with --turbo-fast-api-calls:', ...args);
spawnSyncAndAssert(
process.execPath, [
'--turbo-fast-api-calls',
childPath,
...args,
],
spawnArgs,
expectations,
);
console.log('Testing with --no-turbo-fast-api-calls:', ...args);
spawnSyncAndAssert(
process.execPath,
[
'--no-turbo-fast-api-calls',
childPath,
...args,
],
spawnArgs,
expectations,
);
}
module.exports = {
testWasiPreview1,
};

View File

@@ -0,0 +1,982 @@
'use strict';
const assert = require('assert');
const fixtures = require('../common/fixtures');
const fs = require('fs');
const fsPromises = fs.promises;
const path = require('path');
const events = require('events');
const os = require('os');
const { inspect } = require('util');
const { Worker } = require('worker_threads');
const workerPath = path.join(__dirname, 'wpt/worker.js');
function getBrowserProperties() {
const { node: version } = process.versions; // e.g. 18.13.0, 20.0.0-nightly202302078e6e215481
const release = /^\d+\.\d+\.\d+$/.test(version);
const browser = {
browser_channel: release ? 'stable' : 'experimental',
browser_version: version,
};
return browser;
}
/**
* Return one of three expected values
* https://github.com/web-platform-tests/wpt/blob/1c6ff12/tools/wptrunner/wptrunner/tests/test_update.py#L953-L958
*/
function getOs() {
switch (os.type()) {
case 'Linux':
return 'linux';
case 'Darwin':
return 'mac';
case 'Windows_NT':
return 'win';
default:
throw new Error('Unsupported os.type()');
}
}
// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3705
function sanitizeUnpairedSurrogates(str) {
return str.replace(
/([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
function(_, low, prefix, high) {
let output = prefix || ''; // Prefix may be undefined
const string = low || high; // Only one of these alternates can match
for (let i = 0; i < string.length; i++) {
output += codeUnitStr(string[i]);
}
return output;
});
}
function codeUnitStr(char) {
return 'U+' + char.charCodeAt(0).toString(16);
}
class ReportResult {
#startTime;
constructor(name) {
this.test = name;
this.status = 'OK';
this.subtests = [];
this.#startTime = Date.now();
}
addSubtest(name, status, message) {
const subtest = {
status,
// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3722
name: sanitizeUnpairedSurrogates(name),
};
if (message) {
// https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L4506
subtest.message = sanitizeUnpairedSurrogates(message);
}
this.subtests.push(subtest);
return subtest;
}
finish(status) {
this.status = status ?? 'OK';
this.duration = Date.now() - this.#startTime;
}
}
// Generates a report that can be uploaded to wpt.fyi.
// Checkout https://github.com/web-platform-tests/wpt.fyi/tree/main/api#results-creation
// for more details.
class WPTReport {
constructor(path) {
this.filename = `report-${path.replaceAll('/', '-')}.json`;
/** @type {Map<string, ReportResult>} */
this.results = new Map();
this.time_start = Date.now();
}
/**
* Get or create a ReportResult for a test spec.
* @param {WPTTestSpec} spec
*/
getResult(spec) {
const name = `/${spec.getRelativePath()}${spec.variant}`;
if (this.results.has(name)) {
return this.results.get(name);
}
const result = new ReportResult(name);
this.results.set(name, result);
return result;
}
write() {
this.time_end = Date.now();
const results = Array.from(this.results.values())
.map((result) => {
const url = new URL(result.test, 'http://wpt');
url.pathname = url.pathname.replace(/\.js$/, '.html');
result.test = url.href.slice(url.origin.length);
return result;
});
/**
* Return required and some optional properties
* https://github.com/web-platform-tests/wpt.fyi/blob/60da175/api/README.md?plain=1#L331-L335
*/
this.run_info = {
product: 'node.js',
...getBrowserProperties(),
revision: process.env.WPT_REVISION || 'unknown',
os: getOs(),
};
fs.writeFileSync(`out/wpt/${this.filename}`, JSON.stringify({
time_start: this.time_start,
time_end: this.time_end,
run_info: this.run_info,
results: results,
}));
}
}
// https://github.com/web-platform-tests/wpt/blob/HEAD/resources/testharness.js
// TODO: get rid of this half-baked harness in favor of the one
// pulled from WPT
const harnessMock = {
test: (fn, desc) => {
try {
fn();
} catch (err) {
console.error(`In ${desc}:`);
throw err;
}
},
assert_equals: assert.strictEqual,
assert_true: (value, message) => assert.strictEqual(value, true, message),
assert_false: (value, message) => assert.strictEqual(value, false, message),
assert_throws: (code, func, desc) => {
assert.throws(func, function(err) {
return typeof err === 'object' &&
'name' in err &&
err.name.startsWith(code.name);
}, desc);
},
assert_array_equals: assert.deepStrictEqual,
assert_unreached(desc) {
assert.fail(`Reached unreachable code: ${desc}`);
},
};
class ResourceLoader {
constructor(path) {
this.path = path;
}
toRealFilePath(from, url) {
// We need to patch this to load the WebIDL parser
url = url.replace(
'/resources/WebIDLParser.js',
'/resources/webidl2/lib/webidl2.js',
);
const base = path.dirname(from);
return url.startsWith('/') ?
fixtures.path('wpt', url) :
fixtures.path('wpt', base, url);
}
/**
* Load a resource in test/fixtures/wpt specified with a URL
* @param {string} from the path of the file loading this resource,
* relative to the WPT folder.
* @param {string} url the url of the resource being loaded.
*/
read(from, url) {
const file = this.toRealFilePath(from, url);
return fs.readFileSync(file, 'utf8');
}
/**
* Load a resource in test/fixtures/wpt specified with a URL
* @param {string} from the path of the file loading this resource,
* relative to the WPT folder.
* @param {string} url the url of the resource being loaded.
*/
async readAsFetch(from, url) {
const file = this.toRealFilePath(from, url);
const data = await fsPromises.readFile(file);
return {
ok: true,
arrayBuffer() { return data.buffer; },
json() { return JSON.parse(data.toString()); },
text() { return data.toString(); },
};
}
}
class StatusRule {
constructor(key, value, pattern) {
this.key = key;
this.requires = value.requires || [];
this.fail = value.fail;
this.skip = value.skip;
if (pattern) {
this.pattern = this.transformPattern(pattern);
}
// TODO(joyeecheung): implement this
this.scope = value.scope;
this.comment = value.comment;
}
/**
* Transform a filename pattern into a RegExp
* @param {string} pattern
* @returns {RegExp}
*/
transformPattern(pattern) {
const result = path.normalize(pattern).replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&');
return new RegExp(result.replace('*', '.*'));
}
}
class StatusRuleSet {
constructor() {
// We use two sets of rules to speed up matching
this.exactMatch = {};
this.patternMatch = [];
}
/**
* @param {object} rules
*/
addRules(rules) {
for (const key of Object.keys(rules)) {
if (key.includes('*')) {
this.patternMatch.push(new StatusRule(key, rules[key], key));
} else {
const normalizedPath = path.normalize(key);
this.exactMatch[normalizedPath] = new StatusRule(key, rules[key]);
}
}
}
match(file) {
const result = [];
const exact = this.exactMatch[file];
if (exact) {
result.push(exact);
}
for (const item of this.patternMatch) {
if (item.pattern.test(file)) {
result.push(item);
}
}
return result;
}
}
// A specification of WPT test
class WPTTestSpec {
#content;
/**
* @param {string} mod name of the WPT module, e.g.
* 'html/webappapis/microtask-queuing'
* @param {string} filename path of the test, relative to mod, e.g.
* 'test.any.js'
* @param {StatusRule[]} rules
* @param {string} variant test file variant
*/
constructor(mod, filename, rules, variant = '') {
this.module = mod;
this.filename = filename;
this.variant = variant;
this.requires = new Set();
this.failedTests = [];
this.flakyTests = [];
this.skipReasons = [];
for (const item of rules) {
if (item.requires.length) {
for (const req of item.requires) {
this.requires.add(req);
}
}
if (Array.isArray(item.fail?.expected)) {
this.failedTests.push(...item.fail.expected);
}
if (Array.isArray(item.fail?.flaky)) {
this.failedTests.push(...item.fail.flaky);
this.flakyTests.push(...item.fail.flaky);
}
if (item.skip) {
this.skipReasons.push(item.skip);
}
}
this.failedTests = [...new Set(this.failedTests)];
this.flakyTests = [...new Set(this.flakyTests)];
this.skipReasons = [...new Set(this.skipReasons)];
}
/**
* @param {string} mod
* @param {string} filename
* @param {StatusRule[]} rules
*/
static from(mod, filename, rules) {
const spec = new WPTTestSpec(mod, filename, rules);
const meta = spec.getMeta();
return meta.variant?.map((variant) => new WPTTestSpec(mod, filename, rules, variant)) || [spec];
}
getRelativePath() {
return path.join(this.module, this.filename);
}
getAbsolutePath() {
return fixtures.path('wpt', this.getRelativePath());
}
/**
* @returns {string}
*/
getContent() {
this.#content ||= fs.readFileSync(this.getAbsolutePath(), 'utf8');
return this.#content;
}
/**
* @returns {{ script?: string[]; variant?: string[]; [key: string]: string }} parsed META tags of a spec file
*/
getMeta() {
const matches = this.getContent().match(/\/\/ META: .+/g);
if (!matches) {
return {};
}
const result = {};
for (const match of matches) {
const parts = match.match(/\/\/ META: ([^=]+?)=(.+)/);
const key = parts[1];
const value = parts[2];
if (key === 'script' || key === 'variant') {
if (result[key]) {
result[key].push(value);
} else {
result[key] = [value];
}
} else {
result[key] = value;
}
}
return result;
}
}
const kIntlRequirement = {
none: 0,
small: 1,
full: 2,
// TODO(joyeecheung): we may need to deal with --with-intl=system-icu
};
class BuildRequirement {
constructor() {
this.currentIntl = kIntlRequirement.none;
if (process.config.variables.v8_enable_i18n_support === 0) {
this.currentIntl = kIntlRequirement.none;
return;
}
// i18n enabled
if (process.config.variables.icu_small) {
this.currentIntl = kIntlRequirement.small;
} else {
this.currentIntl = kIntlRequirement.full;
}
// Not using common.hasCrypto because of the global leak checks
this.hasCrypto = Boolean(process.versions.openssl) &&
!process.env.NODE_SKIP_CRYPTO;
}
/**
* @param {Set} requires
* @returns {string|false} The config that the build is lacking, or false
*/
isLacking(requires) {
const current = this.currentIntl;
if (requires.has('full-icu') && current !== kIntlRequirement.full) {
return 'full-icu';
}
if (requires.has('small-icu') && current < kIntlRequirement.small) {
return 'small-icu';
}
if (requires.has('crypto') && !this.hasCrypto) {
return 'crypto';
}
return false;
}
}
const buildRequirements = new BuildRequirement();
class StatusLoader {
/**
* @param {string} path relative path of the WPT subset
*/
constructor(path) {
this.path = path;
this.rules = new StatusRuleSet();
/** @type {WPTTestSpec[]} */
this.specs = [];
}
/**
* Grep for all .*.js file recursively in a directory.
* @param {string} dir
*/
grep(dir) {
let result = [];
const list = fs.readdirSync(dir);
for (const file of list) {
const filepath = path.join(dir, file);
const stat = fs.statSync(filepath);
if (stat.isDirectory()) {
const list = this.grep(filepath);
result = result.concat(list);
} else {
if (!(/\.\w+\.js$/.test(filepath))) {
continue;
}
result.push(filepath);
}
}
return result;
}
load() {
const dir = path.join(__dirname, '..', 'wpt');
let statusFile = path.join(dir, 'status', `${this.path}.json`);
let result;
if (fs.existsSync(statusFile)) {
result = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
} else {
statusFile = path.join(dir, 'status', `${this.path}.cjs`);
result = require(statusFile);
}
this.rules.addRules(result);
const subDir = fixtures.path('wpt', this.path);
const list = this.grep(subDir);
for (const file of list) {
const relativePath = path.relative(subDir, file);
const match = this.rules.match(relativePath);
this.specs.push(...WPTTestSpec.from(this.path, relativePath, match));
}
}
}
const kPass = 'pass';
const kFail = 'fail';
const kSkip = 'skip';
const kTimeout = 'timeout';
const kIncomplete = 'incomplete';
const kUncaught = 'uncaught';
const NODE_UNCAUGHT = 100;
const limit = (concurrency) => {
let running = 0;
const queue = [];
const execute = async (fn) => {
if (running < concurrency) {
running++;
try {
await fn();
} finally {
running--;
if (queue.length > 0) {
execute(queue.shift());
}
}
} else {
queue.push(fn);
}
};
return execute;
};
class WPTRunner {
constructor(path, { concurrency = os.availableParallelism() - 1 || 1 } = {}) {
this.path = path;
this.resource = new ResourceLoader(path);
this.concurrency = concurrency;
this.flags = [];
this.globalThisInitScripts = [];
this.initScript = null;
this.status = new StatusLoader(path);
this.status.load();
this.specs = new Set(this.status.specs);
this.results = {};
this.inProgress = new Set();
this.workers = new Map();
this.unexpectedFailures = [];
if (process.env.WPT_REPORT != null) {
this.report = new WPTReport(path);
}
}
/**
* Sets the Node.js flags passed to the worker.
* @param {string[]} flags
*/
setFlags(flags) {
this.flags = flags;
}
/**
* Sets a script to be run in the worker before executing the tests.
* @param {string} script
*/
setInitScript(script) {
this.initScript = script;
}
/**
* Set the scripts modifier for each script.
* @param {(meta: { code: string, filename: string }) => void} modifier
*/
setScriptModifier(modifier) {
this.scriptsModifier = modifier;
}
/**
* @param {WPTTestSpec} spec
*/
fullInitScript(spec) {
const url = new URL(`/${spec.getRelativePath().replace(/\.js$/, '.html')}${spec.variant}`, 'http://wpt');
const title = spec.getMeta().title;
let { initScript } = this;
initScript = `${initScript}\n\n//===\nglobalThis.location = new URL("${url.href}");`;
if (title) {
initScript = `${initScript}\n\n//===\nglobalThis.META_TITLE = "${title}";`;
}
if (this.globalThisInitScripts.length === null) {
return initScript;
}
const globalThisInitScript = this.globalThisInitScripts.join('\n\n//===\n');
if (initScript === null) {
return globalThisInitScript;
}
return `${globalThisInitScript}\n\n//===\n${initScript}`;
}
/**
* Pretend the runner is run in `name`'s environment (globalThis).
* @param {'Window'} name
* @see {@link https://github.com/nodejs/node/blob/24673ace8ae196bd1c6d4676507d6e8c94cf0b90/test/fixtures/wpt/resources/idlharness.js#L654-L671}
*/
pretendGlobalThisAs(name) {
switch (name) {
case 'Window': {
this.globalThisInitScripts.push('globalThis.Window = Object.getPrototypeOf(globalThis).constructor;');
this.loadLazyGlobals();
break;
}
// TODO(XadillaX): implement `ServiceWorkerGlobalScope`,
// `DedicateWorkerGlobalScope`, etc.
//
// e.g. `ServiceWorkerGlobalScope` should implement dummy
// `addEventListener` and so on.
default: throw new Error(`Invalid globalThis type ${name}.`);
}
}
loadLazyGlobals() {
const lazyProperties = [
'DOMException',
'Performance', 'PerformanceEntry', 'PerformanceMark', 'PerformanceMeasure',
'PerformanceObserver', 'PerformanceObserverEntryList', 'PerformanceResourceTiming',
'Blob', 'atob', 'btoa',
'MessageChannel', 'MessagePort', 'MessageEvent',
'EventTarget', 'Event',
'AbortController', 'AbortSignal',
'performance',
'TransformStream', 'TransformStreamDefaultController',
'WritableStream', 'WritableStreamDefaultController', 'WritableStreamDefaultWriter',
'ReadableStream', 'ReadableStreamDefaultReader',
'ReadableStreamBYOBReader', 'ReadableStreamBYOBRequest',
'ReadableByteStreamController', 'ReadableStreamDefaultController',
'ByteLengthQueuingStrategy', 'CountQueuingStrategy',
'TextEncoder', 'TextDecoder', 'TextEncoderStream', 'TextDecoderStream',
'CompressionStream', 'DecompressionStream',
];
if (Boolean(process.versions.openssl) && !process.env.NODE_SKIP_CRYPTO) {
lazyProperties.push('crypto', 'Crypto', 'CryptoKey', 'SubtleCrypto');
}
const script = lazyProperties.map((name) => `globalThis.${name};`).join('\n');
this.globalThisInitScripts.push(script);
}
// TODO(joyeecheung): work with the upstream to port more tests in .html
// to .js.
async runJsTests() {
const queue = this.buildQueue();
const run = limit(this.concurrency);
for (const spec of queue) {
const content = spec.getContent();
const meta = spec.getMeta(content);
const absolutePath = spec.getAbsolutePath();
const relativePath = spec.getRelativePath();
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
// Scripts specified with the `// META: script=` header
const scriptsToRun = meta.script?.map((script) => {
const obj = {
filename: this.resource.toRealFilePath(relativePath, script),
code: this.resource.read(relativePath, script),
};
this.scriptsModifier?.(obj);
return obj;
}) ?? [];
// The actual test
const obj = {
code: content,
filename: absolutePath,
};
this.scriptsModifier?.(obj);
scriptsToRun.push(obj);
run(async () => {
const worker = new Worker(workerPath, {
execArgv: this.flags,
workerData: {
testRelativePath: relativePath,
wptRunner: __filename,
wptPath: this.path,
initScript: this.fullInitScript(spec),
harness: {
code: fs.readFileSync(harnessPath, 'utf8'),
filename: harnessPath,
},
scriptsToRun,
needsGc: !!meta.script?.find((script) => script === '/common/gc.js'),
},
});
this.inProgress.add(spec);
this.workers.set(spec, worker);
const reportResult = this.report?.getResult(spec);
worker.on('message', (message) => {
switch (message.type) {
case 'result':
return this.resultCallback(spec, message.result, reportResult);
case 'completion':
return this.completionCallback(spec, message.status, reportResult);
default:
throw new Error(`Unexpected message from worker: ${message.type}`);
}
});
worker.on('error', (err) => {
if (!this.inProgress.has(spec)) {
// The test is already finished. Ignore errors that occur after it.
// This can happen normally, for example in timers tests.
return;
}
// Generate a subtest failure for visibility.
// No need to record this synthetic failure with wpt.fyi.
this.fail(
spec,
{
status: NODE_UNCAUGHT,
name: 'evaluation in WPTRunner.runJsTests()',
message: err.message,
stack: inspect(err),
},
kUncaught,
);
// Mark the whole test as failed in wpt.fyi report.
reportResult?.finish('ERROR');
this.inProgress.delete(spec);
});
await events.once(worker, 'exit').catch(() => {});
});
}
process.on('exit', () => {
for (const spec of this.inProgress) {
// No need to record this synthetic failure with wpt.fyi.
this.fail(spec, { name: 'Incomplete' }, kIncomplete);
// Mark the whole test as failed in wpt.fyi report.
const reportResult = this.report?.getResult(spec);
reportResult?.finish('ERROR');
}
inspect.defaultOptions.depth = Infinity;
// Sorts the rules to have consistent output
console.log('');
console.log(JSON.stringify(Object.keys(this.results).sort().reduce(
(obj, key) => {
obj[key] = this.results[key];
return obj;
},
{},
), null, 2));
const failures = [];
let expectedFailures = 0;
let skipped = 0;
for (const [key, item] of Object.entries(this.results)) {
if (item.fail?.unexpected) {
failures.push(key);
}
if (item.fail?.expected) {
expectedFailures++;
}
if (item.skip) {
skipped++;
}
}
const unexpectedPasses = [];
for (const specs of queue) {
const key = specs.filename;
// File has no expected failures
if (!specs.failedTests.length) {
continue;
}
// File was (maybe even conditionally) skipped
if (this.results[key]?.skip) {
continue;
}
// Full check: every expected to fail test is present
if (specs.failedTests.some((expectedToFail) => {
if (specs.flakyTests.includes(expectedToFail)) {
return false;
}
return this.results[key]?.fail?.expected?.includes(expectedToFail) !== true;
})) {
unexpectedPasses.push(key);
continue;
}
}
this.report?.write();
const ran = queue.length;
const total = ran + skipped;
const passed = ran - expectedFailures - failures.length;
console.log('');
console.log(`Ran ${ran}/${total} tests, ${skipped} skipped,`,
`${passed} passed, ${expectedFailures} expected failures,`,
`${failures.length} unexpected failures,`,
`${unexpectedPasses.length} unexpected passes`);
if (failures.length > 0) {
const file = path.join('test', 'wpt', 'status', `${this.path}.json`);
throw new Error(
`Found ${failures.length} unexpected failures. ` +
`Consider updating ${file} for these files:\n${failures.join('\n')}`);
}
if (unexpectedPasses.length > 0) {
const file = path.join('test', 'wpt', 'status', `${this.path}.json`);
throw new Error(
`Found ${unexpectedPasses.length} unexpected passes. ` +
`Consider updating ${file} for these files:\n${unexpectedPasses.join('\n')}`);
}
});
}
// Map WPT test status to strings
getTestStatus(status) {
switch (status) {
case 1:
return kFail;
case 2:
return kTimeout;
case 3:
return kIncomplete;
case NODE_UNCAUGHT:
return kUncaught;
default:
return kPass;
}
}
/**
* Report the status of each specific test case (there could be multiple
* in one test file).
* @param {WPTTestSpec} spec
* @param {Test} test The Test object returned by WPT harness
* @param {ReportResult} reportResult The report result object
*/
resultCallback(spec, test, reportResult) {
const status = this.getTestStatus(test.status);
if (status !== kPass) {
this.fail(spec, test, status, reportResult);
} else {
this.succeed(test, status, reportResult);
}
}
/**
* Report the status of each WPT test (one per file)
* @param {WPTTestSpec} spec
* @param {object} harnessStatus - The status object returned by WPT harness.
* @param {ReportResult} reportResult The report result object
*/
completionCallback(spec, harnessStatus, reportResult) {
const status = this.getTestStatus(harnessStatus.status);
// Treat it like a test case failure
if (status === kTimeout) {
// No need to record this synthetic failure with wpt.fyi.
this.fail(spec, { name: 'WPT testharness timeout' }, kTimeout);
// Mark the whole test as TIMEOUT in wpt.fyi report.
reportResult?.finish('TIMEOUT');
} else if (status !== kPass) {
// No need to record this synthetic failure with wpt.fyi.
this.fail(spec, {
status: status,
name: 'WPT test harness error',
message: harnessStatus.message,
stack: harnessStatus.stack,
}, status);
// Mark the whole test as ERROR in wpt.fyi report.
reportResult?.finish('ERROR');
} else {
reportResult?.finish();
}
this.inProgress.delete(spec);
// Always force termination of the worker. Some tests allocate resources
// that would otherwise keep it alive.
this.workers.get(spec).terminate();
}
addTestResult(spec, item) {
let result = this.results[spec.filename];
result ||= this.results[spec.filename] = {};
if (item.status === kSkip) {
// { filename: { skip: 'reason' } }
result[kSkip] = item.reason;
} else {
// { filename: { fail: { expected: [ ... ],
// unexpected: [ ... ] } }}
result[item.status] ||= {};
const key = item.expected ? 'expected' : 'unexpected';
result[item.status][key] ||= [];
const hasName = result[item.status][key].includes(item.name);
if (!hasName) {
result[item.status][key].push(item.name);
}
}
}
succeed(test, status, reportResult) {
console.log(`[${status.toUpperCase()}] ${test.name}`);
reportResult?.addSubtest(test.name, 'PASS');
}
fail(spec, test, status, reportResult) {
const expected = spec.failedTests.includes(test.name);
if (expected) {
console.log(`[EXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`);
} else {
console.log(`[UNEXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`);
}
if (status === kFail || status === kUncaught) {
console.log(test.message);
console.log(test.stack);
}
const command = `${process.execPath} ${process.execArgv}` +
` ${require.main.filename} '${spec.filename}${spec.variant}'`;
console.log(`Command: ${command}\n`);
reportResult?.addSubtest(test.name, 'FAIL', test.message);
this.addTestResult(spec, {
name: test.name,
expected,
status: kFail,
reason: test.message || status,
});
}
skip(spec, reasons) {
const joinedReasons = reasons.join('; ');
console.log(`[SKIPPED] ${spec.filename}${spec.variant}: ${joinedReasons}`);
this.addTestResult(spec, {
status: kSkip,
reason: joinedReasons,
});
}
buildQueue() {
const queue = [];
let argFilename;
let argVariant;
if (process.argv[2]) {
([argFilename, argVariant = ''] = process.argv[2].split('?'));
}
for (const spec of this.specs) {
if (argFilename) {
if (spec.filename === argFilename && (!argVariant || spec.variant.substring(1) === argVariant)) {
queue.push(spec);
}
continue;
}
if (spec.skipReasons.length > 0) {
this.skip(spec, spec.skipReasons);
continue;
}
const lackingSupport = buildRequirements.isLacking(spec.requires);
if (lackingSupport) {
this.skip(spec, [ `requires ${lackingSupport}` ]);
continue;
}
queue.push(spec);
}
// If the tests are run as `node test/wpt/test-something.js subset.any.js`,
// only `subset.any.js` (all variants) will be run by the runner.
// If the tests are run as `node test/wpt/test-something.js 'subset.any.js?1-10'`,
// only the `?1-10` variant of `subset.any.js` will be run by the runner.
if (argFilename && queue.length === 0) {
throw new Error(`${process.argv[2]} not found!`);
}
return queue;
}
}
module.exports = {
harness: harnessMock,
ResourceLoader,
WPTRunner,
};

View File

@@ -0,0 +1,70 @@
'use strict';
const { runInNewContext, runInThisContext } = require('vm');
const { setFlagsFromString } = require('v8');
const { parentPort, workerData } = require('worker_threads');
const { ResourceLoader } = require(workerData.wptRunner);
const resource = new ResourceLoader(workerData.wptPath);
if (workerData.needsGc) {
// See https://github.com/nodejs/node/issues/16595#issuecomment-340288680
setFlagsFromString('--expose-gc');
globalThis.gc = runInNewContext('gc');
}
globalThis.self = global;
globalThis.GLOBAL = {
isWindow() { return false; },
isShadowRealm() { return false; },
};
globalThis.require = require;
// This is a mock for non-fetch tests that use fetch to resolve
// a relative fixture file.
// Actual Fetch API WPTs are executed in nodejs/undici.
globalThis.fetch = function fetch(file) {
return resource.readAsFetch(workerData.testRelativePath, file);
};
if (workerData.initScript) {
runInThisContext(workerData.initScript);
}
runInThisContext(workerData.harness.code, {
filename: workerData.harness.filename,
});
// eslint-disable-next-line no-undef
add_result_callback((result) => {
parentPort.postMessage({
type: 'result',
result: {
status: result.status,
name: result.name,
message: result.message,
stack: result.stack,
},
});
});
// Keep the event loop alive
const timeout = setTimeout(() => {
parentPort.postMessage({
type: 'completion',
status: { status: 2 },
});
}, 2 ** 31 - 1); // Max timeout is 2^31-1, when overflown the timeout is set to 1.
// eslint-disable-next-line no-undef
add_completion_callback((_, status) => {
clearTimeout(timeout);
parentPort.postMessage({
type: 'completion',
status,
});
});
for (const scriptToRun of workerData.scriptsToRun) {
runInThisContext(scriptToRun.code, { filename: scriptToRun.filename });
}

Some files were not shown because too many files have changed in this diff Show More