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.
1190 lines
28 KiB
TypeScript
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,
|
|
)
|
|
})
|