mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
210 Commits
ali/react
...
kai/fix-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b2351a12c | ||
|
|
7ee91a9912 | ||
|
|
769c6de751 | ||
|
|
c1a25d0948 | ||
|
|
1baa1b6975 | ||
|
|
1789364215 | ||
|
|
bb33176924 | ||
|
|
4a10bf22f7 | ||
|
|
33d3732d44 | ||
|
|
78861829c9 | ||
|
|
e817928981 | ||
|
|
3fc6ad4982 | ||
|
|
7a623fe3e8 | ||
|
|
f78ac6344b | ||
|
|
94260398b0 | ||
|
|
81690617c0 | ||
|
|
30eda1eca9 | ||
|
|
e0414d0890 | ||
|
|
b191968681 | ||
|
|
2406936f33 | ||
|
|
24a3f96359 | ||
|
|
7a73f14da7 | ||
|
|
6ba0563d2d | ||
|
|
dec572eb4b | ||
|
|
01bbe3070a | ||
|
|
b0a30ca422 | ||
|
|
1e649b4976 | ||
|
|
5949777ec3 | ||
|
|
7f9935a560 | ||
|
|
437d333978 | ||
|
|
c38eca222e | ||
|
|
d93122a656 | ||
|
|
b02fb2463f | ||
|
|
ba5490dafc | ||
|
|
3e085b5540 | ||
|
|
73e98663bb | ||
|
|
d09050127f | ||
|
|
eef79ce772 | ||
|
|
cf960b5c17 | ||
|
|
6603871617 | ||
|
|
e5c5033790 | ||
|
|
07252d1755 | ||
|
|
d9c8f27bf9 | ||
|
|
28830f0294 | ||
|
|
2aee62382f | ||
|
|
4103b738ff | ||
|
|
f73ef54edd | ||
|
|
f9718af6a5 | ||
|
|
f50114332f | ||
|
|
90852a37d5 | ||
|
|
2afb5e635d | ||
|
|
134f66c24d | ||
|
|
f37df906b4 | ||
|
|
ed1f25e5cc | ||
|
|
2646ea0956 | ||
|
|
9fa480ce9b | ||
|
|
83a2c245f3 | ||
|
|
e11a68315b | ||
|
|
f439dacf21 | ||
|
|
544dd2497c | ||
|
|
0e7ed996d3 | ||
|
|
bb8b46507e | ||
|
|
06d37bf644 | ||
|
|
6dd369e66b | ||
|
|
8358f4dc73 | ||
|
|
9dbe40ddba | ||
|
|
f54f4e6ebf | ||
|
|
adc00e0566 | ||
|
|
d3b509e80a | ||
|
|
b11d631e41 | ||
|
|
440111f924 | ||
|
|
ccd72755dc | ||
|
|
59700068d3 | ||
|
|
ac8c6f093b | ||
|
|
a8fa566101 | ||
|
|
7b25ce15eb | ||
|
|
c20c0dea92 | ||
|
|
86d4dbe143 | ||
|
|
83f536f4da | ||
|
|
d11a48398d | ||
|
|
563b3c0339 | ||
|
|
6e7240b6e7 | ||
|
|
2335e35a86 | ||
|
|
bd45a65f2b | ||
|
|
7993f4fa11 | ||
|
|
09a6a11a14 | ||
|
|
c17e05c191 | ||
|
|
9ea9925e9c | ||
|
|
469be87987 | ||
|
|
9490c30d47 | ||
|
|
1d8423ea57 | ||
|
|
0bee1c9b5d | ||
|
|
3a71be377e | ||
|
|
1de2319526 | ||
|
|
8b5fb349dd | ||
|
|
657f5b9f6a | ||
|
|
86e421ad80 | ||
|
|
e34673ca45 | ||
|
|
7c13e637b8 | ||
|
|
9f3b0f754b | ||
|
|
07a391368f | ||
|
|
c30ef2ccc8 | ||
|
|
855b7101e6 | ||
|
|
fceeb228a8 | ||
|
|
85dcebedd7 | ||
|
|
0f29267a3e | ||
|
|
a152557096 | ||
|
|
80b742665e | ||
|
|
7978505b94 | ||
|
|
0dce7366e2 | ||
|
|
71f3089f4d | ||
|
|
07a217f773 | ||
|
|
d4b710287f | ||
|
|
3296a6edc9 | ||
|
|
ab92fc5fab | ||
|
|
f5dc0498f4 | ||
|
|
1c06dbd3ef | ||
|
|
a6d707a74e | ||
|
|
ce469474d8 | ||
|
|
c659b3b7d3 | ||
|
|
a60ae54751 | ||
|
|
3303a5de1f | ||
|
|
d8557ea982 | ||
|
|
e3b5927e73 | ||
|
|
066b1dacc4 | ||
|
|
7a20f515f7 | ||
|
|
afd023ac95 | ||
|
|
1d5da9ef77 | ||
|
|
7110c073ca | ||
|
|
03d945ee05 | ||
|
|
08116e43f4 | ||
|
|
7fab6701e5 | ||
|
|
30fe8d5258 | ||
|
|
a8a2403568 | ||
|
|
664c080d02 | ||
|
|
b5ed0f028f | ||
|
|
1293039002 | ||
|
|
8bb8193a39 | ||
|
|
dffc718b6a | ||
|
|
f15059face | ||
|
|
2d96ec0e21 | ||
|
|
b6dfd89928 | ||
|
|
936ae5a796 | ||
|
|
fe18b871f8 | ||
|
|
a1c4240940 | ||
|
|
dc4177f113 | ||
|
|
d2c4a9a57e | ||
|
|
fb6a48a35f | ||
|
|
5176ab58bc | ||
|
|
a669ff1243 | ||
|
|
91a52311de | ||
|
|
197a26fc16 | ||
|
|
059185f4ba | ||
|
|
838ca008cd | ||
|
|
8a0a88cd42 | ||
|
|
774bb8923d | ||
|
|
8b19e08882 | ||
|
|
84c4f96b3f | ||
|
|
73579e1254 | ||
|
|
19b0fed84f | ||
|
|
699997826f | ||
|
|
528d9a64e8 | ||
|
|
39b442b664 | ||
|
|
249227d4d6 | ||
|
|
71101e1fa3 | ||
|
|
c28d419b25 | ||
|
|
3587391920 | ||
|
|
43d7cfcb23 | ||
|
|
3ba398f482 | ||
|
|
600bc1cbd2 | ||
|
|
f71b440c4d | ||
|
|
d29e72f89c | ||
|
|
e04f461508 | ||
|
|
bdcca417ef | ||
|
|
b8aba83da6 | ||
|
|
b753e4b38b | ||
|
|
a7bc53b92c | ||
|
|
b2080c88f4 | ||
|
|
710f7790cf | ||
|
|
c44eb732ee | ||
|
|
e5e643d8bc | ||
|
|
d612cfff12 | ||
|
|
020c32bc73 | ||
|
|
a240093a97 | ||
|
|
e5ffd66649 | ||
|
|
5bae294c14 | ||
|
|
ea1ddb2740 | ||
|
|
6d1db2c8e9 | ||
|
|
8c571d8949 | ||
|
|
7be1bf3026 | ||
|
|
2d0e0c9195 | ||
|
|
b773e66d67 | ||
|
|
2fee09fc4d | ||
|
|
216e5b3f96 | ||
|
|
ed4175b80e | ||
|
|
a0c2a73730 | ||
|
|
1649c03824 | ||
|
|
49b2de93d0 | ||
|
|
ef4728c267 | ||
|
|
6a440aa946 | ||
|
|
6169f1053a | ||
|
|
85f617f97e | ||
|
|
23dc0fed71 | ||
|
|
afcf7b1eb6 | ||
|
|
2583f33a33 | ||
|
|
dfa2a6b60b | ||
|
|
e66ec2a10b | ||
|
|
eb8d465c50 | ||
|
|
418139358b | ||
|
|
1f5359705e |
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
26
src/bun.js/bindings/napi_finalizer.cpp
Normal file
26
src/bun.js/bindings/napi_finalizer.cpp
Normal 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
|
||||
31
src/bun.js/bindings/napi_finalizer.h
Normal file
31
src/bun.js/bindings/napi_finalizer.h
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
30
src/bun.js/bindings/napi_macros.h
Normal file
30
src/bun.js/bindings/napi_macros.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
1212
src/napi/napi.zig
1212
src/napi/napi.zig
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
43
test/js/third_party/prisma/prisma.test.ts
vendored
43
test/js/third_party/prisma/prisma.test.ts
vendored
@@ -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) => {
|
||||
|
||||
2
test/napi/napi-app/.clangd
Normal file
2
test/napi/napi-app/.clangd
Normal file
@@ -0,0 +1,2 @@
|
||||
CompileFlags:
|
||||
CompilationDatabase: '.'
|
||||
194
test/napi/napi-app/async_tests.cpp
Normal file
194
test/napi/napi-app/async_tests.cpp
Normal 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
|
||||
11
test/napi/napi-app/async_tests.h
Normal file
11
test/napi/napi-app/async_tests.h
Normal 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
|
||||
@@ -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\")"],
|
||||
|
||||
171
test/napi/napi-app/class_test.cpp
Normal file
171
test/napi/napi-app/class_test.cpp
Normal 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
|
||||
12
test/napi/napi-app/class_test.h
Normal file
12
test/napi/napi-app/class_test.h
Normal 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
|
||||
173
test/napi/napi-app/conversion_tests.cpp
Normal file
173
test/napi/napi-app/conversion_tests.cpp
Normal 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
|
||||
12
test/napi/napi-app/conversion_tests.h
Normal file
12
test/napi/napi-app/conversion_tests.h
Normal 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
|
||||
306
test/napi/napi-app/js_test_helpers.cpp
Normal file
306
test/napi/napi-app/js_test_helpers.cpp
Normal 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
|
||||
13
test/napi/napi-app/js_test_helpers.h
Normal file
13
test/napi/napi-app/js_test_helpers.h
Normal 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
137
test/napi/napi-app/leak-fixture.js
generated
Normal 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,
|
||||
);
|
||||
})();
|
||||
193
test/napi/napi-app/leak_tests.cpp
Normal file
193
test/napi/napi-app/leak_tests.cpp
Normal 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
|
||||
12
test/napi/napi-app/leak_tests.h
Normal file
12
test/napi/napi-app/leak_tests.h
Normal 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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
8
test/napi/napi-app/napi_with_version.h
Normal file
8
test/napi/napi-app/napi_with_version.h
Normal 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;
|
||||
381
test/napi/napi-app/standalone_tests.cpp
Normal file
381
test/napi/napi-app/standalone_tests.cpp
Normal 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
|
||||
11
test/napi/napi-app/standalone_tests.h
Normal file
11
test/napi/napi-app/standalone_tests.h
Normal 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
|
||||
89
test/napi/napi-app/utils.h
Normal file
89
test/napi/napi-app/utils.h
Normal 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";
|
||||
}
|
||||
}
|
||||
167
test/napi/napi-app/wrap_tests.cpp
Normal file
167
test/napi/napi-app/wrap_tests.cpp
Normal 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
|
||||
11
test/napi/napi-app/wrap_tests.h
Normal file
11
test/napi/napi-app/wrap_tests.h
Normal 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
|
||||
@@ -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
1
test/napi/node-napi-tests/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
5
test/napi/node-napi-tests/README.md
Normal file
5
test/napi/node-napi-tests/README.md
Normal 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.
|
||||
329
test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof-2.js
Normal file
329
test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof-2.js
Normal 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);
|
||||
}
|
||||
93
test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof.js
Normal file
93
test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof.js
Normal 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();
|
||||
55
test/napi/node-napi-tests/test/README.md
Normal file
55
test/napi/node-napi-tests/test/README.md
Normal 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
|
||||
1179
test/napi/node-napi-tests/test/common/README.md
Normal file
1179
test/napi/node-napi-tests/test/common/README.md
Normal file
File diff suppressed because it is too large
Load Diff
23
test/napi/node-napi-tests/test/common/arraystream.js
Normal file
23
test/napi/node-napi-tests/test/common/arraystream.js
Normal 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;
|
||||
104
test/napi/node-napi-tests/test/common/assertSnapshot.js
Normal file
104
test/napi/node-napi-tests/test/common/assertSnapshot.js
Normal 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,
|
||||
};
|
||||
54
test/napi/node-napi-tests/test/common/benchmark.js
Normal file
54
test/napi/node-napi-tests/test/common/benchmark.js
Normal 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;
|
||||
156
test/napi/node-napi-tests/test/common/child_process.js
Normal file
156
test/napi/node-napi-tests/test/common/child_process.js
Normal 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,
|
||||
};
|
||||
28
test/napi/node-napi-tests/test/common/countdown.js
Normal file
28
test/napi/node-napi-tests/test/common/countdown.js
Normal 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;
|
||||
50
test/napi/node-napi-tests/test/common/cpu-prof.js
Normal file
50
test/napi/node-napi-tests/test/common/cpu-prof.js
Normal 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,
|
||||
};
|
||||
114
test/napi/node-napi-tests/test/common/crypto.js
Normal file
114
test/napi/node-napi-tests/test/common/crypto.js
Normal 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,
|
||||
};
|
||||
183
test/napi/node-napi-tests/test/common/debugger.js
Normal file
183
test/napi/node-napi-tests/test/common/debugger.js
Normal 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;
|
||||
341
test/napi/node-napi-tests/test/common/dns.js
Normal file
341
test/napi/node-napi-tests/test/common/dns.js
Normal 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,
|
||||
};
|
||||
59
test/napi/node-napi-tests/test/common/fixtures.js
Normal file
59
test/napi/node-napi-tests/test/common/fixtures.js
Normal 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');
|
||||
},
|
||||
};
|
||||
5
test/napi/node-napi-tests/test/common/fixtures.mjs
Normal file
5
test/napi/node-napi-tests/test/common/fixtures.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
import fixtures from "./fixtures.js";
|
||||
|
||||
const { fixturesDir, path, fileURL, readSync, readKey } = fixtures;
|
||||
|
||||
export { fileURL, fixturesDir, path, readKey, readSync };
|
||||
192
test/napi/node-napi-tests/test/common/gc.js
Normal file
192
test/napi/node-napi-tests/test/common/gc.js
Normal 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,
|
||||
};
|
||||
146
test/napi/node-napi-tests/test/common/globals.js
Normal file
146
test/napi/node-napi-tests/test/common/globals.js
Normal 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,
|
||||
};
|
||||
329
test/napi/node-napi-tests/test/common/heap.js
Normal file
329
test/napi/node-napi-tests/test/common/heap.js
Normal 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,
|
||||
};
|
||||
32
test/napi/node-napi-tests/test/common/hijackstdio.js
Normal file
32
test/napi/node-napi-tests/test/common/hijackstdio.js
Normal 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'),
|
||||
};
|
||||
129
test/napi/node-napi-tests/test/common/http2.js
Normal file
129
test/napi/node-napi-tests/test/common/http2.js
Normal 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,
|
||||
};
|
||||
1210
test/napi/node-napi-tests/test/common/index.js
Normal file
1210
test/napi/node-napi-tests/test/common/index.js
Normal file
File diff suppressed because it is too large
Load Diff
110
test/napi/node-napi-tests/test/common/index.mjs
Normal file
110
test/napi/node-napi-tests/test/common/index.mjs
Normal 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,
|
||||
};
|
||||
549
test/napi/node-napi-tests/test/common/inspector-helper.js
Normal file
549
test/napi/node-napi-tests/test/common/inspector-helper.js
Normal 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,
|
||||
};
|
||||
58
test/napi/node-napi-tests/test/common/internet.js
Normal file
58
test/napi/node-napi-tests/test/common/internet.js
Normal 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,
|
||||
};
|
||||
57
test/napi/node-napi-tests/test/common/measure-memory.js
Normal file
57
test/napi/node-napi-tests/test/common/measure-memory.js
Normal 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,
|
||||
};
|
||||
23
test/napi/node-napi-tests/test/common/net.js
Normal file
23
test/napi/node-napi-tests/test/common/net.js
Normal 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,
|
||||
};
|
||||
3
test/napi/node-napi-tests/test/common/package.json
Normal file
3
test/napi/node-napi-tests/test/common/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
138
test/napi/node-napi-tests/test/common/process-exit-code-cases.js
Normal file
138
test/napi/node-napi-tests/test/common/process-exit-code-cases.js
Normal 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;
|
||||
67
test/napi/node-napi-tests/test/common/prof.js
Normal file
67
test/napi/node-napi-tests/test/common/prof.js
Normal 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,
|
||||
};
|
||||
346
test/napi/node-napi-tests/test/common/report.js
Normal file
346
test/napi/node-napi-tests/test/common/report.js
Normal 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 };
|
||||
27
test/napi/node-napi-tests/test/common/require-as.js
Normal file
27
test/napi/node-napi-tests/test/common/require-as.js
Normal 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);
|
||||
}
|
||||
144
test/napi/node-napi-tests/test/common/sea.js
Normal file
144
test/napi/node-napi-tests/test/common/sea.js
Normal 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,
|
||||
};
|
||||
50
test/napi/node-napi-tests/test/common/shared-lib-util.js
Normal file
50
test/napi/node-napi-tests/test/common/shared-lib-util.js
Normal 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,
|
||||
};
|
||||
65
test/napi/node-napi-tests/test/common/snapshot.js
Normal file
65
test/napi/node-napi-tests/test/common/snapshot.js
Normal 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,
|
||||
};
|
||||
12
test/napi/node-napi-tests/test/common/tick.js
Normal file
12
test/napi/node-napi-tests/test/common/tick.js
Normal 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);
|
||||
};
|
||||
176
test/napi/node-napi-tests/test/common/tls.js
Normal file
176
test/napi/node-napi-tests/test/common/tls.js
Normal 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;
|
||||
95
test/napi/node-napi-tests/test/common/tmpdir.js
Normal file
95
test/napi/node-napi-tests/test/common/tmpdir.js
Normal 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,
|
||||
};
|
||||
24
test/napi/node-napi-tests/test/common/udp.js
Normal file
24
test/napi/node-napi-tests/test/common/udp.js
Normal 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,
|
||||
};
|
||||
70
test/napi/node-napi-tests/test/common/v8.js
Normal file
70
test/napi/node-napi-tests/test/common/v8.js
Normal 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,
|
||||
};
|
||||
43
test/napi/node-napi-tests/test/common/wasi.js
Normal file
43
test/napi/node-napi-tests/test/common/wasi.js
Normal 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,
|
||||
};
|
||||
982
test/napi/node-napi-tests/test/common/wpt.js
Normal file
982
test/napi/node-napi-tests/test/common/wpt.js
Normal 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,
|
||||
};
|
||||
70
test/napi/node-napi-tests/test/common/wpt/worker.js
Normal file
70
test/napi/node-napi-tests/test/common/wpt/worker.js
Normal 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
Reference in New Issue
Block a user