napi-rs/examples/napi/__tests__/values.spec.ts
Louis 43415251b8
feat(napi): allow Reference as a class method param (#1966)
As of before this commit, there was a lock in the codegen preventing Reference
from being used as a function argument outside of a Reference<Self>.

This changes it, allowing Reference of any class to be added as a class method
argument anywhere. It has the same limitations as reference, as in it requires
the class to have been created with a factory or constructor. This change
implements FromNapiValue on Reference, which will unwrap the class and call the
existing from_value_ptr method. It also updated typegen so that we only emit
the reference type if we're in an impl block that doesn't match the Reference
we're getting. This ensures that typegen works as expected with the previous
behaviour.
2024-02-22 22:37:50 +08:00

1190 lines
28 KiB
TypeScript

import { exec } from 'node:child_process'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spy } from 'sinon'
import type { AliasedStruct, Animal as AnimalClass } from '../index.js'
import { test } from './test.framework.js'
const __dirname = join(fileURLToPath(import.meta.url), '..')
const {
DEFAULT_COST,
add,
fibonacci,
call0,
call1,
call2,
apply0,
apply1,
callFunction,
callFunctionWithArg,
callFunctionWithArgAndCtx,
createReferenceOnFunction,
referenceAsCallback,
contains,
concatLatin1,
concatStr,
concatUtf16,
roundtripStr,
getNums,
getWords,
sumNums,
getMapping,
sumMapping,
getBtreeMapping,
sumBtreeMapping,
getIndexMapping,
sumIndexMapping,
indexmapPassthrough,
getCwd,
Animal,
Kind,
NinjaTurtle,
ClassWithFactory,
CustomNumEnum,
Context,
GetterSetterWithClosures,
enumToI32,
listObjKeys,
createObj,
mapOption,
readFile,
throwError,
customStatusCode,
panic,
readPackageJson,
getPackageJsonName,
getBuffer,
getEmptyBuffer,
asyncBufferToArray,
readFileAsync,
eitherStringOrNumber,
returnEither,
either3,
either4,
withoutAbortController,
withAbortController,
asyncMultiTwo,
bigintAdd,
createBigInt,
createBigIntI64,
bigintGetU64AsString,
callThreadsafeFunction,
threadsafeFunctionThrowError,
threadsafeFunctionClosureCapture,
tsfnCallWithCallback,
tsfnAsyncCall,
tsfnThrowFromJs,
asyncPlus100,
getGlobal,
getUndefined,
getNull,
setSymbolInObj,
createSymbol,
createSymbolFor,
threadsafeFunctionFatalMode,
createExternal,
getExternal,
mutateExternal,
createExternalString,
xxh2,
xxh3,
xxh64Alias,
tsRename,
acceptSlice,
u8ArrayToArray,
i8ArrayToArray,
u16ArrayToArray,
i16ArrayToArray,
u32ArrayToArray,
i32ArrayToArray,
u64ArrayToArray,
i64ArrayToArray,
f32ArrayToArray,
f64ArrayToArray,
convertU32Array,
createExternalTypedArray,
mutateTypedArray,
receiveAllOptionalObject,
fnReceivedAliased,
ALIAS,
appendBuffer,
returnNull,
returnUndefined,
Dog,
Bird,
Assets,
receiveStrictObject,
receiveClassOrNumber,
JsClassForEither,
receiveMutClassOrNumber,
getStrFromObject,
returnJsFunction,
testSerdeRoundtrip,
testSerdeBigNumberPrecision,
createObjWithProperty,
receiveObjectOnlyFromJs,
dateToNumber,
chronoDateToMillis,
derefUint8Array,
chronoDateAdd1Minute,
bufferPassThrough,
arrayBufferPassThrough,
JsRepo,
JsRemote,
CssStyleSheet,
CatchOnConstructor,
CatchOnConstructor2,
asyncReduceBuffer,
callbackReturnPromise,
callbackReturnPromiseAndSpawn,
returnEitherClass,
eitherFromOption,
eitherFromObjects,
overrideIndividualArgOnFunction,
overrideIndividualArgOnFunctionWithCbArg,
createObjectWithClassField,
receiveObjectWithClassField,
AnotherClassForEither,
receiveDifferentClass,
getNumArr,
getNestedNumArr,
CustomFinalize,
plusOne,
Width,
captureErrorInCallback,
bigintFromI128,
bigintFromI64,
acceptThreadsafeFunction,
acceptThreadsafeFunctionFatal,
acceptThreadsafeFunctionTupleArgs,
promiseInEither,
runScript,
tsfnReturnPromise,
tsfnReturnPromiseTimeout,
returnFromSharedCrate,
chronoNativeDateTime,
chronoNativeDateTimeReturn,
throwAsyncError,
getModuleFileName,
throwSyntaxError,
} = (await import('../index.js')).default
const Napi4Test = Number(process.versions.napi) >= 4 ? test : test.skip
test('export const', (t) => {
t.is(DEFAULT_COST, 12)
})
test('number', (t) => {
t.is(add(1, 2), 3)
t.is(fibonacci(5), 5)
t.throws(
// @ts-expect-error
() => fibonacci(''),
void 0,
'Expect value to be Number, but received String',
)
})
test('string', (t) => {
t.true(contains('hello', 'ell'))
t.false(contains('John', 'jn'))
t.is(concatStr('涽¾DEL'), '涽¾DEL + Rust 🦀 string!')
t.is(concatLatin1('涽¾DEL'), '涽¾DEL + Rust 🦀 string!')
t.is(
concatUtf16('JavaScript 🌳 你好 napi'),
'JavaScript 🌳 你好 napi + Rust 🦀 string!',
)
t.is(
roundtripStr('what up?!\u0000after the NULL'),
'what up?!\u0000after the NULL',
)
})
test('array', (t) => {
t.deepEqual(getNums(), [1, 1, 2, 3, 5, 8])
t.deepEqual(getWords(), ['foo', 'bar'])
t.is(sumNums([1, 2, 3, 4, 5]), 15)
t.deepEqual(getNumArr(), [1, 2])
t.deepEqual(getNestedNumArr(), [[[1]], [[1]]])
})
test('map', (t) => {
t.deepEqual(getMapping(), { a: 101, b: 102 })
t.is(sumMapping({ a: 101, b: 102 }), 203)
t.deepEqual(getBtreeMapping(), { a: 101, b: 102 })
t.is(sumBtreeMapping({ a: 101, b: 102 }), 203)
t.deepEqual(getIndexMapping(), { a: 101, b: 102 })
t.is(sumIndexMapping({ a: 101, b: 102 }), 203)
t.deepEqual(indexmapPassthrough({ a: 101, b: 102 }), { a: 101, b: 102 })
})
test('enum', (t) => {
t.deepEqual([Kind.Dog, Kind.Cat, Kind.Duck], [0, 1, 2])
t.is(enumToI32(CustomNumEnum.Eight), 8)
})
test('function call', async (t) => {
t.is(
call0(() => 42),
42,
)
t.is(
call1((a) => a + 10, 42),
52,
)
t.is(
call2((a, b) => a + b, 42, 10),
52,
)
const ctx = new Animal(Kind.Dog, '旺财')
apply0(ctx, function (this: AnimalClass) {
this.name = '可乐'
})
t.is(ctx.name, '可乐')
const ctx2 = new Animal(Kind.Dog, '旺财')
apply1(
ctx2,
function (this: AnimalClass, name: string) {
this.name = name
},
'可乐',
)
t.is(ctx2.name, '可乐')
t.is(
callFunction(() => 42),
42,
)
t.is(
callFunctionWithArg((a, b) => a + b, 42, 10),
52,
)
const ctx3 = new Animal(Kind.Dog, '旺财')
callFunctionWithArgAndCtx(
ctx3,
function (this: AnimalClass, name: string) {
this.name = name
},
'可乐',
)
t.is(ctx3.name, '可乐')
const cbSpy = spy()
await createReferenceOnFunction(cbSpy)
t.is(cbSpy.callCount, 1)
t.is(
referenceAsCallback((a, b) => a + b, 42, 10),
52,
)
})
test('class', (t) => {
const dog = new Animal(Kind.Dog, '旺财')
t.is(dog.name, '旺财')
t.is(dog.kind, Kind.Dog)
t.is(dog.whoami(), 'Dog: 旺财')
t.notThrows(() => {
const rawMethod = dog.whoami
dog.whoami = function (...args) {
return rawMethod.apply(this, args)
}
})
dog.name = '可乐'
t.is(dog.name, '可乐')
t.deepEqual(dog.returnOtherClass(), new Dog('Doge'))
t.deepEqual(dog.returnOtherClassWithCustomConstructor(), new Bird('parrot'))
t.is(
dog.overrideIndividualArgOnMethod('Jafar', { n: 'Iago' }).name,
'Jafar-Iago',
)
t.is(dog.returnOtherClassWithCustomConstructor().getCount(), 1234)
t.is(dog.type, Kind.Dog)
dog.type = Kind.Cat
t.is(dog.type, Kind.Cat)
const assets = new Assets()
t.is(assets.get(1)?.filePath, 1)
const turtle = NinjaTurtle.newRaph()
t.is(turtle.returnThis(), turtle)
t.is(NinjaTurtle.isInstanceOf(turtle), true)
// Inject this to function
const width = new Width(1)
t.is(plusOne.call(width), 2)
t.throws(() => {
// @ts-expect-error
plusOne.call('')
})
t.notThrows(() => {
new CatchOnConstructor()
})
if (!process.env.TEST_ZIG_CROSS) {
t.throws(
() => {
new CatchOnConstructor2()
},
(() =>
process.env.WASI_TEST
? undefined
: {
message: 'CatchOnConstructor2 panic',
})(),
)
}
})
test('async self in class', async (t) => {
const b = new Bird('foo')
t.is(await b.getNameAsync(), 'foo')
})
test('class factory', (t) => {
const duck = ClassWithFactory.withName('Default')
t.is(duck.name, 'Default')
const ret = duck.setName('D')
t.is(ret.name, 'D')
t.is(ret, duck)
duck.name = '周黑鸭'
t.is(duck.name, '周黑鸭')
const doge = Animal.withKind(Kind.Dog)
t.is(doge.name, 'Default')
doge.name = '旺财'
t.is(doge.name, '旺财')
const error = t.throws(() => new ClassWithFactory())
t.true(
error?.message.startsWith(
'Class contains no `constructor`, can not new it!',
),
)
})
test('async class factory', async (t) => {
const instance = await ClassWithFactory.with4Name('foo')
t.is(instance.name, 'foo-4')
const instance2 = await ClassWithFactory.with4NameResult('foo')
t.is(instance2.name, 'foo-4')
})
test('class constructor return Result', (t) => {
const c = new Context()
t.is(c.method(), 'not empty')
})
test('class default field is TypedArray', (t) => {
const c = new Context()
t.deepEqual(c.buffer, new Uint8Array([0, 1, 2, 3]))
const fixture = new Uint8Array([0, 1, 2, 3, 4, 5, 6])
const c2 = Context.withBuffer(fixture)
t.is(c2.buffer, fixture)
})
test('class Factory return Result', (t) => {
const c = Context.withData('not empty')
t.is(c.method(), 'not empty')
})
test('class in object field', (t) => {
const obj = createObjectWithClassField()
t.is(obj.bird.name, 'Carolyn')
t.is(receiveObjectWithClassField(obj), obj.bird)
})
test('custom finalize class', (t) => {
t.notThrows(() => new CustomFinalize(200, 200))
})
test('should be able to create object reference and shared reference', (t) => {
const repo = new JsRepo('.')
t.is(repo.remote().name(), 'origin')
t.is(new JsRemote(repo).name(), 'origin')
})
test('should be able to into_reference', (t) => {
const rules = ['body: { color: red }', 'div: { color: blue }']
const sheet = new CssStyleSheet('test.css', rules)
t.is(sheet.rules, sheet.rules)
t.deepEqual(sheet.rules.getRules(), rules)
t.is(sheet.rules.parentStyleSheet, sheet)
t.is(sheet.rules.name, 'test.css')
const anotherStyleSheet = sheet.anotherCssStyleSheet()
t.is(anotherStyleSheet.rules, sheet.rules)
})
test('callback', (t) => {
getCwd((cwd) => {
t.is(cwd, process.env.WASI_TEST ? '/' : process.cwd())
})
t.throws(
// @ts-expect-error
() => getCwd(),
void 0,
'Expect value to be Function, but received Undefined',
)
readFile((err, content) => {
t.is(err, undefined)
t.is(content, 'hello world')
})
captureErrorInCallback(
() => {
throw new Error('Testing')
},
(err) => {
t.is((err as Error).message, 'Testing')
},
)
})
test('return function', (t) => {
return new Promise<void>((resolve) => {
returnJsFunction()((err: Error | undefined, content: string) => {
t.is(err, undefined)
t.is(content, 'hello world')
resolve()
})
})
})
Napi4Test('callback function return Promise', async (t) => {
const cbSpy = spy()
await callbackReturnPromise<string>(() => '1', spy)
t.is(cbSpy.callCount, 0)
await callbackReturnPromise(
() => Promise.resolve('42'),
(err, res) => {
t.is(err, null)
cbSpy(res)
},
)
t.is(cbSpy.callCount, 1)
t.deepEqual(cbSpy.args, [['42']])
})
Napi4Test('callback function return Promise and spawn', async (t) => {
const finalReturn = await callbackReturnPromiseAndSpawn((input) =>
Promise.resolve(`${input} world`),
)
t.is(finalReturn, 'Hello world 😼')
})
test('object', (t) => {
t.deepEqual(listObjKeys({ name: 'John Doe', age: 20 }), ['name', 'age'])
t.deepEqual(createObj(), { test: 1 })
})
test('get str from object', (t) => {
t.notThrows(() => getStrFromObject())
})
test('create object from Property', (t) => {
const obj = createObjWithProperty()
t.true(obj.value instanceof ArrayBuffer)
t.is(obj.getter, 42)
})
test('global', (t) => {
t.is(getGlobal(), global)
})
test('get undefined', (t) => {
for (const _ of Array.from({ length: 100 })) {
t.is(getUndefined(), undefined)
}
})
test('get null', (t) => {
for (const _ of Array.from({ length: 100 })) {
t.is(getNull(), null)
}
})
test('return Null', (t) => {
t.is(returnNull(), null)
})
test('return Undefined', (t) => {
t.is(returnUndefined(), undefined)
})
test('pass symbol in', (t) => {
const sym = Symbol('test')
const obj = setSymbolInObj(sym)
// @ts-expect-error
t.is(obj[sym], 'a symbol')
})
test('create symbol', (t) => {
t.is(createSymbol().toString(), 'Symbol(a symbol)')
})
test('Option', (t) => {
t.is(mapOption(null), null)
t.is(mapOption(3), 4)
})
test('Result', (t) => {
t.throws(() => throwError(), void 0, 'Manual Error')
if (!process.env.SKIP_UNWIND_TEST) {
t.throws(() => panic(), void 0, `Don't panic`)
}
})
test('Async error with stack trace', async (t) => {
const err = await t.throwsAsync(() => throwAsyncError())
t.not(err?.stack, undefined)
t.deepEqual(err!.message, 'Async Error')
if (!process.env.WASI_TEST) {
t.regex(err!.stack!, /.+at .+values\.spec\.ts:\d+:\d+.+/gm)
}
})
test('custom status code in Error', (t) => {
t.throws(() => customStatusCode(), {
code: 'Panic',
})
})
test('function ts type override', (t) => {
// @ts-expect-error
t.deepEqual(tsRename({ foo: 1, bar: 2, baz: 2 }), ['foo', 'bar', 'baz'])
})
test('function individual ts arg type override', (t) => {
t.is(
overrideIndividualArgOnFunction('someStr', () => 'anotherStr', 42),
'oia: someStr-42-anotherStr',
)
t.deepEqual(
overrideIndividualArgOnFunctionWithCbArg(
(town, opt) => `im: ${town}-${opt}`,
89,
),
'im: World(89)-null',
)
})
test('option object', (t) => {
t.notThrows(() => receiveAllOptionalObject())
t.notThrows(() => receiveAllOptionalObject({}))
})
test('should throw if object type is not matched', (t) => {
// @ts-expect-error
const err1 = t.throws(() => receiveStrictObject({ name: 1 }))
t.is(
err1?.message,
'Failed to convert JavaScript value `Number 1 ` into rust type `String`',
)
// @ts-expect-error
const err2 = t.throws(() => receiveStrictObject({ bar: 1 }))
t.is(err2!.message, 'Missing field `name`')
})
test('aliased rust struct and enum', (t) => {
const a = ALIAS.A
const b: AliasedStruct = {
a,
b: 1,
}
t.notThrows(() => fnReceivedAliased(b, ALIAS.B))
})
test('serde-json', (t) => {
if (process.env.WASI_TEST) {
t.pass()
return
}
const packageJson = readPackageJson()
t.is(packageJson.name, '@examples/napi')
t.is(packageJson.version, '0.0.0')
t.snapshot(Object.keys(packageJson.devDependencies!).sort())
t.is(getPackageJsonName(packageJson), '@examples/napi')
})
test('serde-roundtrip', (t) => {
t.is(testSerdeRoundtrip(1), 1)
t.is(testSerdeRoundtrip(1.2), 1.2)
t.is(testSerdeRoundtrip(-1), -1)
t.deepEqual(testSerdeRoundtrip([1, 1.2, -1]), [1, 1.2, -1])
t.deepEqual(testSerdeRoundtrip({ a: 1, b: 1.2, c: -1 }), {
a: 1,
b: 1.2,
c: -1,
})
t.throws(() => testSerdeRoundtrip(NaN))
t.is(testSerdeRoundtrip(null), null)
let err = t.throws(() => testSerdeRoundtrip(undefined))
t.is(err?.message, 'undefined cannot be represented as a serde_json::Value')
err = t.throws(() => testSerdeRoundtrip(() => {}))
t.is(
err!.message,
'JS functions cannot be represented as a serde_json::Value',
)
err = t.throws(() => testSerdeRoundtrip(Symbol.for('foo')))
t.is(err!.message, 'JS symbols cannot be represented as a serde_json::Value')
})
test('serde-large-number-precision', (t) => {
t.is(testSerdeBigNumberPrecision('12345').number, 12345)
t.is(
testSerdeBigNumberPrecision('123456789012345678901234567890').number,
1.2345678901234568e29,
)
t.is(
testSerdeBigNumberPrecision('123456789012345678901234567890.123456789')
.number,
1.2345678901234568e29,
)
t.is(
testSerdeBigNumberPrecision('109775245175819965').number.toString(),
'109775245175819965',
)
})
test('buffer', (t) => {
let buf = getBuffer()
t.is(buf.toString('utf-8'), 'Hello world')
buf = appendBuffer(buf)
t.is(buf.toString('utf-8'), 'Hello world!')
const a = getEmptyBuffer()
const b = getEmptyBuffer()
t.is(a.toString(), '')
t.is(b.toString(), '')
// @ts-expect-error
t.true(Array.isArray(asyncBufferToArray(Buffer.from([1, 2, 3]).buffer)))
})
test('TypedArray', (t) => {
t.is(acceptSlice(new Uint8Array([1, 2, 3])), 3n)
t.deepEqual(u8ArrayToArray(new Uint8Array([1, 2, 3])), [1, 2, 3])
t.deepEqual(i8ArrayToArray(new Int8Array([1, 2, 3])), [1, 2, 3])
t.deepEqual(u16ArrayToArray(new Uint16Array([1, 2, 3])), [1, 2, 3])
t.deepEqual(i16ArrayToArray(new Int16Array([1, 2, 3])), [1, 2, 3])
t.deepEqual(u32ArrayToArray(new Uint32Array([1, 2, 3])), [1, 2, 3])
t.deepEqual(i32ArrayToArray(new Int32Array([1, 2, 3])), [1, 2, 3])
t.deepEqual(u64ArrayToArray(new BigUint64Array([1n, 2n, 3n])), [1n, 2n, 3n])
t.deepEqual(i64ArrayToArray(new BigInt64Array([1n, 2n, 3n])), [1, 2, 3])
t.deepEqual(f32ArrayToArray(new Float32Array([1, 2, 3])), [1, 2, 3])
t.deepEqual(f64ArrayToArray(new Float64Array([1, 2, 3])), [1, 2, 3])
const bird = new Bird('Carolyn')
t.is(bird.acceptSliceMethod(new Uint8Array([1, 2, 3])), 3)
})
test('reset empty buffer', (t) => {
const empty = getEmptyBuffer()
const shared = new ArrayBuffer(0)
const buffer = Buffer.from(shared)
t.notThrows(() => {
buffer.set(empty)
})
})
test('convert typedarray to vec', (t) => {
const input = new Uint32Array([1, 2, 3, 4, 5])
t.deepEqual(convertU32Array(input), Array.from(input))
})
test('create external TypedArray', (t) => {
t.deepEqual(createExternalTypedArray(), new Uint32Array([1, 2, 3, 4, 5]))
})
test('mutate TypedArray', (t) => {
if (process.env.WASI_TEST) {
t.pass()
return
}
const input = new Float32Array([1, 2, 3, 4, 5])
mutateTypedArray(input)
t.deepEqual(input, new Float32Array([2.0, 4.0, 6.0, 8.0, 10.0]))
})
test('deref uint8 array', (t) => {
t.is(
derefUint8Array(new Uint8Array([1, 2]), new Uint8ClampedArray([3, 4])),
4,
)
})
test('async', async (t) => {
if (process.env.WASI_TEST) {
t.pass()
return
}
const bufPromise = readFileAsync(join(__dirname, '../package.json'))
await t.notThrowsAsync(bufPromise)
const buf = await bufPromise
const { name } = JSON.parse(buf.toString())
t.is(name, '@examples/napi')
await t.throwsAsync(() => readFileAsync('some_nonexist_path.file'))
})
test('async move', async (t) => {
t.is(await asyncMultiTwo(2), 4)
})
test('buffer passthrough', async (t) => {
const fixture = Buffer.from('hello world')
const ret = await bufferPassThrough(fixture)
t.deepEqual(ret, fixture)
})
test('arraybuffer passthrough', async (t) => {
const fixture = new Uint8Array([1, 2, 3, 4, 5])
const ret = await arrayBufferPassThrough(fixture)
t.deepEqual(ret, fixture)
})
test('async reduce buffer', async (t) => {
const input = [1, 2, 3, 4, 5, 6]
const fixture = Buffer.from(input)
t.is(
await asyncReduceBuffer(fixture),
input.reduce((acc, cur) => acc + cur),
)
})
test('either', (t) => {
t.is(eitherStringOrNumber(2), 2)
t.is(eitherStringOrNumber('hello'), 'hello'.length)
})
test('return either', (t) => {
t.is(returnEither(2), 2)
t.is(returnEither(42), '42')
})
test('receive class reference in either', (t) => {
const c = new JsClassForEither()
t.is(receiveClassOrNumber(1), 2)
t.is(receiveClassOrNumber(c), 100)
t.is(receiveMutClassOrNumber(c), 100)
})
test('receive different class', (t) => {
// TODO: fix the napi_unwrap error from the emnapi
if (process.env.WASI_TEST) {
t.pass()
return
}
const a = new JsClassForEither()
const b = new AnotherClassForEither()
t.is(receiveDifferentClass(a), 42)
t.is(receiveDifferentClass(b), 100)
})
test('return either class', (t) => {
t.is(returnEitherClass(1), 1)
t.true(returnEitherClass(-1) instanceof JsClassForEither)
})
test('either from option', (t) => {
t.true(eitherFromOption() instanceof JsClassForEither)
})
test('either from objects', (t) => {
t.is(eitherFromObjects({ foo: 1 }), 'A')
t.is(eitherFromObjects({ bar: 2 }), 'B')
t.is(eitherFromObjects({ baz: 3 }), 'C')
})
test('either3', (t) => {
t.is(either3(2), 2)
t.is(either3('hello'), 'hello'.length)
t.is(either3(true), 1)
t.is(either3(false), 0)
})
test('either4', (t) => {
t.is(either4(2), 2)
t.is(either4('hello'), 'hello'.length)
t.is(either4(true), 1)
t.is(either4(false), 0)
t.is(either4({ v: 1 }), 1)
t.is(either4({ v: 'world' }), 'world'.length)
})
test('external', (t) => {
const FX = 42
const ext = createExternal(FX)
t.is(getExternal(ext), FX)
mutateExternal(ext, FX + 1)
t.is(getExternal(ext), FX + 1)
// @ts-expect-error
t.throws(() => getExternal({}))
const ext2 = createExternalString('wtf')
// @ts-expect-error
const e = t.throws(() => getExternal(ext2))
t.is(
e?.message,
'T on `get_value_external` is not the type of wrapped object',
)
})
test('should be able to run script', async (t) => {
t.is(runScript(`1 + 1`), 2)
t.is(await runScript(`Promise.resolve(1)`), 1)
})
test('should be able to return object from shared crate', (t) => {
t.deepEqual(returnFromSharedCrate(), {
value: 42,
})
})
const AbortSignalTest =
typeof AbortController !== 'undefined' ? test : test.skip
AbortSignalTest('async task without abort controller', async (t) => {
t.is(await withoutAbortController(1, 2), 3)
})
AbortSignalTest('async task with abort controller', async (t) => {
const ctrl = new AbortController()
const promise = withAbortController(1, 2, ctrl.signal)
try {
ctrl.abort()
await promise
t.fail('Should throw AbortError')
} catch (err: unknown) {
t.is((err as Error).message, 'AbortError')
}
})
AbortSignalTest('abort resolved task', async (t) => {
const ctrl = new AbortController()
await withAbortController(1, 2, ctrl.signal).then(() => ctrl.abort())
t.pass('should not throw')
})
const BigIntTest = typeof BigInt !== 'undefined' ? test : test.skip
BigIntTest('BigInt add', (t) => {
t.is(bigintAdd(BigInt(1), BigInt(2)), BigInt(3))
})
BigIntTest('create BigInt', (t) => {
t.is(createBigInt(), BigInt('-3689348814741910323300'))
})
BigIntTest('create BigInt i64', (t) => {
t.is(createBigIntI64(), BigInt(100))
})
BigIntTest('BigInt get_u64', (t) => {
t.is(bigintGetU64AsString(BigInt(0)), '0')
})
BigIntTest('js mod test', (t) => {
t.is(xxh64Alias(Buffer.from('hello world')), BigInt('1116'))
t.is(xxh3.xxh3_64(Buffer.from('hello world')), BigInt('1116'))
t.is(xxh3.xxh128(Buffer.from('hello world')), BigInt('1116'))
t.is(xxh2.xxh2Plus(1, 2), 3)
t.is(xxh2.xxh3Xxh64Alias(Buffer.from('hello world')), BigInt('1116'))
t.is(xxh3.ALIGNMENT, 16)
const xx3 = new xxh3.Xxh3()
xx3.update(Buffer.from('hello world'))
t.is(xx3.digest(), BigInt('1116'))
})
BigIntTest('from i128 i64', (t) => {
t.is(bigintFromI64(), BigInt('100'))
t.is(bigintFromI128(), BigInt('-100'))
})
Napi4Test('call thread safe function', (t) => {
let i = 0
let value = 0
return new Promise((resolve) => {
callThreadsafeFunction((err, v) => {
t.is(err, null)
i++
value += v
if (i === 100) {
resolve()
t.is(
value,
Array.from({ length: 100 }, (_, i) => i + 1).reduce((a, b) => a + b),
)
}
})
})
})
Napi4Test('throw error from thread safe function', async (t) => {
const throwPromise = new Promise((_, reject) => {
threadsafeFunctionThrowError(reject)
})
const err = await t.throwsAsync(throwPromise)
t.is(err?.message, 'ThrowFromNative')
})
Napi4Test('thread safe function closure capture data', (t) => {
return new Promise((resolve) => {
threadsafeFunctionClosureCapture(() => {
resolve()
t.pass()
})
})
})
Napi4Test('resolve value from thread safe function fatal mode', async (t) => {
const tsfnFatalMode = new Promise<boolean>((resolve) => {
threadsafeFunctionFatalMode(resolve)
})
t.true(await tsfnFatalMode)
})
Napi4Test('throw error from thread safe function fatal mode', (t) => {
const p = exec('node ./tsfn-error.cjs', {
cwd: __dirname,
})
let stderr = Buffer.from([])
p.stderr?.on('data', (data) => {
stderr = Buffer.concat([stderr, Buffer.from(data)])
})
return new Promise<void>((resolve) => {
p.on('exit', (code) => {
t.is(code, 1)
const stderrMsg = stderr.toString('utf8')
console.info(stderrMsg)
t.true(stderrMsg.includes(`Error: Generic tsfn error`))
resolve()
})
})
})
Napi4Test('await Promise in rust', async (t) => {
const fx = 20
const result = await asyncPlus100(
new Promise((resolve) => {
setTimeout(() => resolve(fx), 50)
}),
)
t.is(result, fx + 100)
})
Napi4Test('Promise should reject raw error in rust', async (t) => {
const fxError = new Error('What is Happy Planet')
const err = await t.throwsAsync(() => asyncPlus100(Promise.reject(fxError)))
t.is(err, fxError)
})
Napi4Test('call ThreadsafeFunction with callback', async (t) => {
await t.notThrowsAsync(
() =>
new Promise<void>((resolve) => {
tsfnCallWithCallback(() => {
resolve()
return 'ReturnFromJavaScriptRawCallback'
})
}),
)
})
Napi4Test('async call ThreadsafeFunction', async (t) => {
await t.notThrowsAsync(() =>
tsfnAsyncCall((err, arg1, arg2, arg3) => {
t.is(err, null)
t.is(arg1, 0)
t.is(arg2, 1)
t.is(arg3, 2)
return 'ReturnFromJavaScriptRawCallback'
}),
)
})
test('Throw from ThreadsafeFunction JavaScript callback', async (t) => {
const errMsg = 'ThrowFromJavaScriptRawCallback'
await t.throwsAsync(
() =>
tsfnThrowFromJs(() => {
throw new Error(errMsg)
}),
{
message: errMsg,
},
)
})
Napi4Test('accept ThreadsafeFunction', async (t) => {
await new Promise<void>((resolve, reject) => {
acceptThreadsafeFunction((err, value) => {
if (err) {
reject(err)
} else {
t.is(value, 1)
resolve()
}
})
})
})
Napi4Test('accept ThreadsafeFunction Fatal', async (t) => {
await new Promise<void>((resolve) => {
acceptThreadsafeFunctionFatal((value) => {
t.is(value, 1)
resolve()
})
})
})
Napi4Test('accept ThreadsafeFunction tuple args', async (t) => {
await new Promise<void>((resolve, reject) => {
acceptThreadsafeFunctionTupleArgs((err, num, bool, str) => {
if (err) {
return reject(err)
}
t.is(num, 1)
t.is(bool, false)
t.is(str, 'NAPI-RS')
resolve()
})
})
})
Napi4Test('threadsafe function return Promise and await in Rust', async (t) => {
const value = await tsfnReturnPromise((err, value) => {
if (err) {
throw err
}
return Promise.resolve(value + 2)
})
t.is(value, 5)
await t.throwsAsync(
() =>
tsfnReturnPromiseTimeout((err, value) => {
if (err) {
throw err
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(value + 2)
}, 300)
})
}),
{
message: 'Timeout',
},
)
// trigger Promise.then in Rust after `Promise` is dropped
await new Promise((resolve) => setTimeout(resolve, 400))
})
Napi4Test('object only from js', (t) => {
return new Promise((resolve, reject) => {
receiveObjectOnlyFromJs({
count: 100,
callback: (err: Error | null, count: number) => {
if (err) {
reject(err)
} else {
t.is(count, 100)
resolve()
}
},
})
})
})
Napi4Test('promise in either', async (t) => {
t.is(await promiseInEither(1), false)
t.is(await promiseInEither(20), true)
t.is(await promiseInEither(Promise.resolve(1)), false)
t.is(await promiseInEither(Promise.resolve(20)), true)
// @ts-expect-error
t.throws(() => promiseInEither('1'))
})
const Napi5Test = Number(process.versions.napi) >= 5 ? test : test.skip
Napi5Test('Date test', (t) => {
const fixture = new Date('2016-12-24')
t.is(dateToNumber(fixture), fixture.valueOf())
})
Napi5Test('Date to chrono test', (t) => {
const fixture = new Date('2022-02-09T19:31:55.396Z')
t.is(chronoDateToMillis(fixture), fixture.getTime())
t.deepEqual(
chronoDateAdd1Minute(fixture),
new Date(fixture.getTime() + 60 * 1000),
)
})
Napi5Test('Class with getter setter closures', (t) => {
const instance = new GetterSetterWithClosures()
// @ts-expect-error
instance.name = 'Allie'
t.pass()
// @ts-expect-error
t.is(instance.name, `I'm Allie`)
// @ts-expect-error
t.is(instance.age, 0.3)
})
Napi5Test('Date to chrono::NativeDateTime test', (t) => {
const fixture = new Date()
t.is(chronoNativeDateTime(fixture), fixture.valueOf())
})
Napi5Test('Date from chrono::NativeDateTime test', (t) => {
const fixture = chronoNativeDateTimeReturn()
t.true(fixture instanceof Date)
t.is(fixture?.toISOString(), '2016-12-23T15:25:59.325Z')
})
const Napi9Test = Number(process.versions.napi) >= 9 ? test : test.skip
Napi9Test('create symbol for', (t) => {
t.is(createSymbolFor('foo'), Symbol.for('foo'))
})
Napi9Test('get module file name', (t) => {
if (process.env.WASI_TEST) {
t.pass()
return
}
console.info(getModuleFileName())
t.true(getModuleFileName().includes('examples/napi/index.node'))
})
test('throw syntax error', (t) => {
const message = `Syntax Error: Unexpected token '}'`
const code = 'InvalidCharacterError'
t.throws(
() => throwSyntaxError(message, code),
{
code,
instanceOf: SyntaxError,
},
message,
)
})