sqlite/ext/wasm/testing1.js
stephan ae708b2b01 More experimentation with how client-side config state can be passed on to initialize the JS-side sqlite3 API.
FossilOrigin-Name: ea2acc454c012a62556f6d0623d6eff60736d24aa214a64462b423623ef44d47
2022-08-22 21:37:17 +00:00

1076 lines
42 KiB
JavaScript

/*
2022-05-22
The author disclaims copyright to this source code. In place of a
legal notice, here is a blessing:
* May you do good and not evil.
* May you find forgiveness for yourself and forgive others.
* May you share freely, never taking more than you give.
***********************************************************************
A basic test script for sqlite3-api.js. This file must be run in
main JS thread and sqlite3.js must have been loaded before it.
*/
'use strict';
(function(){
const T = self.SqliteTestUtil;
const toss = function(...args){throw new Error(args.join(' '))};
const debug = console.debug.bind(console);
const eOutput = document.querySelector('#test-output');
const log = console.log.bind(console),
warn = console.warn.bind(console);
const logHtml = function(...args){
log.apply(this, args);
const ln = document.createElement('div');
ln.append(document.createTextNode(args.join(' ')));
eOutput.append(ln);
};
const eqApprox = function(v1,v2,factor=0.05){
//debug('eqApprox',v1, v2);
return v1>=(v2-factor) && v1<=(v2+factor);
};
const testBasicSanity = function(db,sqlite3){
const capi = sqlite3.capi;
log("Basic sanity tests...");
T.assert(Number.isInteger(db.pointer)).
mustThrowMatching(()=>db.pointer=1, /read-only/).
assert(0===capi.sqlite3_extended_result_codes(db.pointer,1)).
assert('main'===db.dbName(0));
let pId;
let st = db.prepare(
new TextEncoder('utf-8').encode("select 3 as a")
/* Testing handling of Uint8Array input */
);
//debug("statement =",st);
try {
T.assert(Number.isInteger(st.pointer))
.mustThrowMatching(()=>st.pointer=1, /read-only/)
.assert(1===db.openStatementCount())
.assert(!st._mayGet)
.assert('a' === st.getColumnName(0))
.assert(1===st.columnCount)
.assert(0===st.parameterCount)
.mustThrow(()=>st.bind(1,null))
.assert(true===st.step())
.assert(3 === st.get(0))
.mustThrow(()=>st.get(1))
.mustThrow(()=>st.get(0,~capi.SQLITE_INTEGER))
.assert(3 === st.get(0,capi.SQLITE_INTEGER))
.assert(3 === st.getInt(0))
.assert('3' === st.get(0,capi.SQLITE_TEXT))
.assert('3' === st.getString(0))
.assert(3.0 === st.get(0,capi.SQLITE_FLOAT))
.assert(3.0 === st.getFloat(0))
.assert(3 === st.get({}).a)
.assert(3 === st.get([])[0])
.assert(3 === st.getJSON(0))
.assert(st.get(0,capi.SQLITE_BLOB) instanceof Uint8Array)
.assert(1===st.get(0,capi.SQLITE_BLOB).length)
.assert(st.getBlob(0) instanceof Uint8Array)
.assert('3'.charCodeAt(0) === st.getBlob(0)[0])
.assert(st._mayGet)
.assert(false===st.step())
.assert(!st._mayGet)
;
pId = st.pointer;
T.assert(0===capi.sqlite3_strglob("*.txt", "foo.txt")).
assert(0!==capi.sqlite3_strglob("*.txt", "foo.xtx")).
assert(0===capi.sqlite3_strlike("%.txt", "foo.txt", 0)).
assert(0!==capi.sqlite3_strlike("%.txt", "foo.xtx", 0));
}finally{
st.finalize();
}
T.assert(!st.pointer)
.assert(0===db.openStatementCount());
let list = [];
db.exec({
sql:['CREATE TABLE t(a,b);',
"INSERT INTO t(a,b) VALUES(1,2),(3,4),",
"(?,?),('blob',X'6869')"/*intentionally missing semicolon to test for
off-by-one bug in string-to-WASM conversion*/],
multi: true,
saveSql: list,
bind: [5,6]
});
//debug("Exec'd SQL:", list);
T.assert(2 === list.length)
.assert('string'===typeof list[1])
.assert(4===db.changes());
if(capi.wasm.bigIntEnabled){
T.assert(4n===db.changes(false,true));
}
let blob = db.selectValue("select b from t where a='blob'");
T.assert(blob instanceof Uint8Array).
assert(0x68===blob[0] && 0x69===blob[1]);
blob = null;
let counter = 0, colNames = [];
list.length = 0;
db.exec(new TextEncoder('utf-8').encode("SELECT a a, b b FROM t"),{
rowMode: 'object',
resultRows: list,
columnNames: colNames,
callback: function(row,stmt){
++counter;
T.assert((row.a%2 && row.a<6) || 'blob'===row.a);
}
});
T.assert(2 === colNames.length)
.assert('a' === colNames[0])
.assert(4 === counter)
.assert(4 === list.length);
list.length = 0;
db.exec("SELECT a a, b b FROM t",{
rowMode: 'array',
callback: function(row,stmt){
++counter;
T.assert(Array.isArray(row))
.assert((0===row[1]%2 && row[1]<7)
|| (row[1] instanceof Uint8Array));
}
});
T.assert(8 === counter);
T.assert(Number.MIN_SAFE_INTEGER ===
db.selectValue("SELECT "+Number.MIN_SAFE_INTEGER)).
assert(Number.MAX_SAFE_INTEGER ===
db.selectValue("SELECT "+Number.MAX_SAFE_INTEGER));
if(capi.wasm.bigIntEnabled){
const mI = capi.wasm.xCall('jaccwabyt_test_int64_max');
const b = BigInt(Number.MAX_SAFE_INTEGER * 2);
T.assert(b === db.selectValue("SELECT "+b)).
assert(b === db.selectValue("SELECT ?", b)).
assert(mI == db.selectValue("SELECT $x", {$x:mI}));
}else{
/* Curiously, the JS spec seems to be off by one with the definitions
of MIN/MAX_SAFE_INTEGER:
https://github.com/emscripten-core/emscripten/issues/17391 */
T.mustThrow(()=>db.selectValue("SELECT "+(Number.MAX_SAFE_INTEGER+1))).
mustThrow(()=>db.selectValue("SELECT "+(Number.MIN_SAFE_INTEGER-1)));
}
st = db.prepare("update t set b=:b where a='blob'");
try {
const ndx = st.getParamIndex(':b');
T.assert(1===ndx);
st.bindAsBlob(ndx, "ima blob").reset(true);
} finally {
st.finalize();
}
try {
throw new sqlite3.WasmAllocError;
}catch(e){
T.assert(e instanceof Error)
.assert(e instanceof sqlite3.WasmAllocError);
}
try {
db.prepare("/*empty SQL*/");
toss("Must not be reached.");
}catch(e){
T.assert(e instanceof sqlite3.SQLite3Error)
.assert(0==e.message.indexOf('Cannot prepare empty'));
}
T.assert(capi.sqlite3_errstr(capi.SQLITE_IOERR_ACCESS).indexOf("I/O")>=0).
assert(capi.sqlite3_errstr(capi.SQLITE_CORRUPT).indexOf('malformed')>0).
assert(capi.sqlite3_errstr(capi.SQLITE_OK) === 'not an error');
// Custom db error message handling via sqlite3_prepare_v2/v3()
if(capi.wasm.exports.sqlite3_wasm_db_error){
log("Testing custom error message via prepare_v3()...");
let rc = capi.sqlite3_prepare_v3(db.pointer, [/*invalid*/], -1, 0, null, null);
T.assert(capi.SQLITE_MISUSE === rc)
.assert(0 === capi.sqlite3_errmsg(db.pointer).indexOf("Invalid SQL"));
log("errmsg =",capi.sqlite3_errmsg(db.pointer));
}
}/*testBasicSanity()*/;
const testUDF = function(db){
db.createFunction("foo",function(a,b){return a+b});
T.assert(7===db.selectValue("select foo(3,4)")).
assert(5===db.selectValue("select foo(3,?)",2)).
assert(5===db.selectValue("select foo(?,?2)",[1,4])).
assert(5===db.selectValue("select foo($a,$b)",{$a:0,$b:5}));
db.createFunction("bar", {
arity: -1,
callback: function(){
var rc = 0;
for(let i = 0; i < arguments.length; ++i) rc += arguments[i];
return rc;
}
}).createFunction({
name: "asis",
callback: (arg)=>arg
});
//log("Testing DB::selectValue() w/ UDF...");
T.assert(0===db.selectValue("select bar()")).
assert(1===db.selectValue("select bar(1)")).
assert(3===db.selectValue("select bar(1,2)")).
assert(-1===db.selectValue("select bar(1,2,-4)")).
assert('hi'===db.selectValue("select asis('hi')"));
T.assert('hi' === db.selectValue("select ?",'hi')).
assert(null===db.selectValue("select null")).
assert(null === db.selectValue("select ?",null)).
assert(null === db.selectValue("select ?",[null])).
assert(null === db.selectValue("select $a",{$a:null})).
assert(eqApprox(3.1,db.selectValue("select 3.0 + 0.1"))).
assert(eqApprox(1.3,db.selectValue("select asis(1 + 0.3)")))
;
//log("Testing binding and UDF propagation of blobs...");
let blobArg = new Uint8Array(2);
blobArg.set([0x68, 0x69], 0);
let blobRc = db.selectValue("select asis(?1)", blobArg);
T.assert(blobRc instanceof Uint8Array).
assert(2 === blobRc.length).
assert(0x68==blobRc[0] && 0x69==blobRc[1]);
blobRc = db.selectValue("select asis(X'6869')");
T.assert(blobRc instanceof Uint8Array).
assert(2 === blobRc.length).
assert(0x68==blobRc[0] && 0x69==blobRc[1]);
blobArg = new Int8Array(2);
blobArg.set([0x68, 0x69]);
//debug("blobArg=",blobArg);
blobRc = db.selectValue("select asis(?1)", blobArg);
T.assert(blobRc instanceof Uint8Array).
assert(2 === blobRc.length);
//debug("blobRc=",blobRc);
T.assert(0x68==blobRc[0] && 0x69==blobRc[1]);
};
const testAttach = function(db){
const resultRows = [];
db.exec({
sql:new TextEncoder('utf-8').encode([
// ^^^ testing string-vs-typedarray handling in execMulti()
"attach 'foo.db' as foo;",
"create table foo.bar(a);",
"insert into foo.bar(a) values(1),(2),(3);",
"select a from foo.bar order by a;"
].join('')),
multi: true,
rowMode: 0,
resultRows
});
T.assert(3===resultRows.length)
.assert(2===resultRows[1]);
T.assert(2===db.selectValue('select a from foo.bar where a>1 order by a'));
db.exec("detach foo");
T.mustThrow(()=>db.exec("select * from foo.bar"));
};
const testIntPtr = function(db,S,Module){
const w = S.capi.wasm;
const stack = w.scopedAllocPush();
let ptrInt;
const origValue = 512;
const ptrValType = 'i32';
try{
ptrInt = w.scopedAlloc(4);
w.setMemValue(ptrInt,origValue, ptrValType);
const cf = w.xGet('jaccwabyt_test_intptr');
const oldPtrInt = ptrInt;
//log('ptrInt',ptrInt);
//log('getMemValue(ptrInt)',w.getMemValue(ptrInt));
T.assert(origValue === w.getMemValue(ptrInt, ptrValType));
const rc = cf(ptrInt);
//log('cf(ptrInt)',rc);
//log('ptrInt',ptrInt);
//log('getMemValue(ptrInt)',w.getMemValue(ptrInt,ptrValType));
T.assert(2*origValue === rc).
assert(rc === w.getMemValue(ptrInt,ptrValType)).
assert(oldPtrInt === ptrInt);
const pi64 = w.scopedAlloc(8)/*ptr to 64-bit integer*/;
const o64 = 0x010203040506/*>32-bit integer*/;
const ptrType64 = 'i64';
if(w.bigIntEnabled){
log("BigInt support is enabled...");
w.setMemValue(pi64, o64, ptrType64);
//log("pi64 =",pi64, "o64 = 0x",o64.toString(16), o64);
const v64 = ()=>w.getMemValue(pi64,ptrType64)
//log("getMemValue(pi64)",v64());
T.assert(v64() == o64);
//T.assert(o64 === w.getMemValue(pi64, ptrType64));
const cf64w = w.xGet('jaccwabyt_test_int64ptr');
cf64w(pi64);
//log("getMemValue(pi64)",v64());
T.assert(v64() == BigInt(2 * o64));
cf64w(pi64);
T.assert(v64() == BigInt(4 * o64));
const biTimes2 = w.xGet('jaccwabyt_test_int64_times2');
T.assert(BigInt(2 * o64) ===
biTimes2(BigInt(o64)/*explicit conv. required to avoid TypeError
in the call :/ */));
const pMin = w.scopedAlloc(16);
const pMax = pMin + 8;
const g64 = (p)=>w.getMemValue(p,ptrType64);
w.setMemValue(pMin, 0, ptrType64);
w.setMemValue(pMax, 0, ptrType64);
const minMaxI64 = [
w.xCall('jaccwabyt_test_int64_min'),
w.xCall('jaccwabyt_test_int64_max')
];
T.assert(minMaxI64[0] < BigInt(Number.MIN_SAFE_INTEGER)).
assert(minMaxI64[1] > BigInt(Number.MAX_SAFE_INTEGER));
//log("int64_min/max() =",minMaxI64, typeof minMaxI64[0]);
w.xCall('jaccwabyt_test_int64_minmax', pMin, pMax);
T.assert(g64(pMin) === minMaxI64[0], "int64 mismatch").
assert(g64(pMax) === minMaxI64[1], "int64 mismatch");
//log("pMin",g64(pMin), "pMax",g64(pMax));
w.setMemValue(pMin, minMaxI64[0], ptrType64);
T.assert(g64(pMin) === minMaxI64[0]).
assert(minMaxI64[0] === db.selectValue("select ?",g64(pMin))).
assert(minMaxI64[1] === db.selectValue("select ?",g64(pMax)));
const rxRange = /out of range for int64/;
T.mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[0] - BigInt(1))},
rxRange).
mustThrowMatching(()=>{db.prepare("select ?").bind(minMaxI64[1] + BigInt(1))},
(e)=>rxRange.test(e.message));
}else{
log("No BigInt support. Skipping related tests.");
log("\"The problem\" here is that we can manipulate, at the byte level,",
"heap memory to set 64-bit values, but we can't get those values",
"back into JS because of the lack of 64-bit integer support.");
}
}finally{
const x = w.scopedAlloc(1), y = w.scopedAlloc(1), z = w.scopedAlloc(1);
//log("x=",x,"y=",y,"z=",z); // just looking at the alignment
w.scopedAllocPop(stack);
}
}/*testIntPtr()*/;
const testStructStuff = function(db,S,M){
const W = S.capi.wasm, C = S;
/** Maintenance reminder: the rest of this function is copy/pasted
from the upstream jaccwabyt tests. */
log("Jaccwabyt tests...");
const MyStructDef = {
sizeof: 16,
members: {
p4: {offset: 0, sizeof: 4, signature: "i"},
pP: {offset: 4, sizeof: 4, signature: "P"},
ro: {offset: 8, sizeof: 4, signature: "i", readOnly: true},
cstr: {offset: 12, sizeof: 4, signature: "s"}
}
};
if(W.bigIntEnabled){
const m = MyStructDef;
m.members.p8 = {offset: m.sizeof, sizeof: 8, signature: "j"};
m.sizeof += m.members.p8.sizeof;
}
const StructType = C.StructBinder.StructType;
const K = C.StructBinder('my_struct',MyStructDef);
T.mustThrowMatching(()=>K(), /via 'new'/).
mustThrowMatching(()=>new K('hi'), /^Invalid pointer/);
const k1 = new K(), k2 = new K();
try {
T.assert(k1.constructor === K).
assert(K.isA(k1)).
assert(k1 instanceof K).
assert(K.prototype.lookupMember('p4').key === '$p4').
assert(K.prototype.lookupMember('$p4').name === 'p4').
mustThrowMatching(()=>K.prototype.lookupMember('nope'), /not a mapped/).
assert(undefined === K.prototype.lookupMember('nope',false)).
assert(k1 instanceof StructType).
assert(StructType.isA(k1)).
assert(K.resolveToInstance(k1.pointer)===k1).
mustThrowMatching(()=>K.resolveToInstance(null,true), /is-not-a my_struct/).
assert(k1 === StructType.instanceForPointer(k1.pointer)).
mustThrowMatching(()=>k1.$ro = 1, /read-only/);
Object.keys(MyStructDef.members).forEach(function(key){
key = K.memberKey(key);
T.assert(0 == k1[key],
"Expecting allocation to zero the memory "+
"for "+key+" but got: "+k1[key]+
" from "+k1.memoryDump());
});
T.assert('number' === typeof k1.pointer).
mustThrowMatching(()=>k1.pointer = 1, /pointer/).
assert(K.instanceForPointer(k1.pointer) === k1);
k1.$p4 = 1; k1.$pP = 2;
T.assert(1 === k1.$p4).assert(2 === k1.$pP);
if(MyStructDef.members.$p8){
k1.$p8 = 1/*must not throw despite not being a BigInt*/;
k1.$p8 = BigInt(Number.MAX_SAFE_INTEGER * 2);
T.assert(BigInt(2 * Number.MAX_SAFE_INTEGER) === k1.$p8);
}
T.assert(!k1.ondispose);
k1.setMemberCString('cstr', "A C-string.");
T.assert(Array.isArray(k1.ondispose)).
assert(k1.ondispose[0] === k1.$cstr).
assert('number' === typeof k1.$cstr).
assert('A C-string.' === k1.memberToJsString('cstr'));
k1.$pP = k2;
T.assert(k1.$pP === k2);
k1.$pP = null/*null is special-cased to 0.*/;
T.assert(0===k1.$pP);
let ptr = k1.pointer;
k1.dispose();
T.assert(undefined === k1.pointer).
assert(undefined === K.instanceForPointer(ptr)).
mustThrowMatching(()=>{k1.$pP=1}, /disposed instance/);
const k3 = new K();
ptr = k3.pointer;
T.assert(k3 === K.instanceForPointer(ptr));
K.disposeAll();
T.assert(ptr).
assert(undefined === k2.pointer).
assert(undefined === k3.pointer).
assert(undefined === K.instanceForPointer(ptr));
}finally{
k1.dispose();
k2.dispose();
}
if(!W.bigIntEnabled){
log("Skipping WasmTestStruct tests: BigInt not enabled.");
return;
}
const ctype = W.xCallWrapped('jaccwabyt_test_ctype_json', 'json');
log("Struct descriptions:",ctype.structs);
const WTStructDesc =
ctype.structs.filter((e)=>'WasmTestStruct'===e.name)[0];
const autoResolvePtr = true /* EXPERIMENTAL */;
if(autoResolvePtr){
WTStructDesc.members.ppV.signature = 'P';
}
const WTStruct = C.StructBinder(WTStructDesc);
log(WTStruct.structName, WTStruct.structInfo);
const wts = new WTStruct();
log("WTStruct.prototype keys:",Object.keys(WTStruct.prototype));
try{
T.assert(wts.constructor === WTStruct).
assert(WTStruct.memberKeys().indexOf('$ppV')>=0).
assert(wts.memberKeys().indexOf('$v8')>=0).
assert(!K.isA(wts)).
assert(WTStruct.isA(wts)).
assert(wts instanceof WTStruct).
assert(wts instanceof StructType).
assert(StructType.isA(wts)).
assert(wts === StructType.instanceForPointer(wts.pointer));
T.assert(wts.pointer>0).assert(0===wts.$v4).assert(0n===wts.$v8).
assert(0===wts.$ppV).assert(0===wts.$xFunc).
assert(WTStruct.instanceForPointer(wts.pointer) === wts);
const testFunc =
W.xGet('jaccwabyt_test_struct'/*name gets mangled in -O3 builds!*/);
let counter = 0;
log("wts.pointer =",wts.pointer);
const wtsFunc = function(arg){
log("This from a JS function called from C, "+
"which itself was called from JS. arg =",arg);
++counter;
T.assert(WTStruct.instanceForPointer(arg) === wts);
if(3===counter){
toss("Testing exception propagation.");
}
}
wts.$v4 = 10; wts.$v8 = 20;
wts.$xFunc = W.installFunction(wtsFunc, wts.memberSignature('xFunc'))
/* ^^^ compiles wtsFunc to WASM and returns its new function pointer */;
T.assert(0===counter).assert(10 === wts.$v4).assert(20n === wts.$v8)
.assert(0 === wts.$ppV).assert('number' === typeof wts.$xFunc)
.assert(0 === wts.$cstr)
.assert(wts.memberIsString('$cstr'))
.assert(!wts.memberIsString('$v4'))
.assert(null === wts.memberToJsString('$cstr'))
.assert(W.functionEntry(wts.$xFunc) instanceof Function);
/* It might seem silly to assert that the values match
what we just set, but recall that all of those property
reads and writes are, via property interceptors,
actually marshaling their data to/from a raw memory
buffer, so merely reading them back is actually part of
testing the struct-wrapping API. */
testFunc(wts.pointer);
log("wts.pointer, wts.$ppV",wts.pointer, wts.$ppV);
T.assert(1===counter).assert(20 === wts.$v4).assert(40n === wts.$v8)
.assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer))
.assert('string' === typeof wts.memberToJsString('cstr'))
.assert(wts.memberToJsString('cstr') === wts.memberToJsString('$cstr'))
.mustThrowMatching(()=>wts.memberToJsString('xFunc'),
/Invalid member type signature for C-string/)
;
testFunc(wts.pointer);
T.assert(2===counter).assert(40 === wts.$v4).assert(80n === wts.$v8)
.assert(autoResolvePtr ? (wts.$ppV === wts) : (wts.$ppV === wts.pointer));
/** The 3rd call to wtsFunc throw from JS, which is called
from C, which is called from JS. Let's ensure that
that exception propagates back here... */
T.mustThrowMatching(()=>testFunc(wts.pointer),/^Testing/);
W.uninstallFunction(wts.$xFunc);
wts.$xFunc = 0;
if(autoResolvePtr){
wts.$ppV = 0;
T.assert(!wts.$ppV);
WTStruct.debugFlags(0x03);
wts.$ppV = wts;
T.assert(wts === wts.$ppV)
WTStruct.debugFlags(0);
}
wts.setMemberCString('cstr', "A C-string.");
T.assert(Array.isArray(wts.ondispose)).
assert(wts.ondispose[0] === wts.$cstr).
assert('A C-string.' === wts.memberToJsString('cstr'));
const ptr = wts.pointer;
wts.dispose();
T.assert(ptr).assert(undefined === wts.pointer).
assert(undefined === WTStruct.instanceForPointer(ptr))
}finally{
wts.dispose();
}
}/*testStructStuff()*/;
const testSqliteStructs = function(db,sqlite3,M){
log("Tinkering with sqlite3_io_methods...");
// https://www.sqlite.org/c3ref/vfs.html
// https://www.sqlite.org/c3ref/io_methods.html
const capi = sqlite3.capi, W = capi.wasm;
const sqlite3_io_methods = capi.sqlite3_io_methods,
sqlite3_vfs = capi.sqlite3_vfs,
sqlite3_file = capi.sqlite3_file;
log("struct sqlite3_file", sqlite3_file.memberKeys());
log("struct sqlite3_vfs", sqlite3_vfs.memberKeys());
log("struct sqlite3_io_methods", sqlite3_io_methods.memberKeys());
const installMethod = function callee(tgt, name, func){
if(1===arguments.length){
return (n,f)=>callee(tgt,n,f);
}
if(!callee.argcProxy){
callee.argcProxy = function(func,sig){
return function(...args){
if(func.length!==arguments.length){
toss("Argument mismatch. Native signature is:",sig);
}
return func.apply(this, args);
}
};
callee.ondisposeRemoveFunc = function(){
if(this.__ondispose){
const who = this;
this.__ondispose.forEach(
(v)=>{
if('number'===typeof v){
try{capi.wasm.uninstallFunction(v)}
catch(e){/*ignore*/}
}else{/*wasm function wrapper property*/
delete who[v];
}
}
);
delete this.__ondispose;
}
};
}/*static init*/
const sigN = tgt.memberSignature(name),
memKey = tgt.memberKey(name);
//log("installMethod",tgt, name, sigN);
if(!tgt.__ondispose){
T.assert(undefined === tgt.ondispose);
tgt.ondispose = [callee.ondisposeRemoveFunc];
tgt.__ondispose = [];
}
const fProxy = callee.argcProxy(func, sigN);
const pFunc = capi.wasm.installFunction(fProxy, tgt.memberSignature(name, true));
tgt[memKey] = pFunc;
/**
ACHTUNG: function pointer IDs are from a different pool than
allocation IDs, starting at 1 and incrementing in steps of 1,
so if we set tgt[memKey] to those values, we'd very likely
later misinterpret them as plain old pointer addresses unless
unless we use some silly heuristic like "all values <5k are
presumably function pointers," or actually perform a function
lookup on every pointer to first see if it's a function. That
would likely work just fine, but would be kludgy.
It turns out that "all values less than X are functions" is
essentially how it works in wasm: a function pointer is
reported to the client as its index into the
__indirect_function_table.
So... once jaccwabyt can be told how to access the
function table, it could consider all pointer values less
than that table's size to be functions. As "real" pointer
values start much, much higher than the function table size,
that would likely work reasonably well. e.g. the object
pointer address for sqlite3's default VFS is (in this local
setup) 65104, whereas the function table has fewer than 600
entries.
*/
const wrapperKey = '$'+memKey;
tgt[wrapperKey] = fProxy;
tgt.__ondispose.push(pFunc, wrapperKey);
//log("tgt.__ondispose =",tgt.__ondispose);
return (n,f)=>callee(tgt, n, f);
}/*installMethod*/;
const installIOMethods = function instm(iom){
(iom instanceof capi.sqlite3_io_methods) || toss("Invalid argument type.");
if(!instm._requireFileArg){
instm._requireFileArg = function(arg,methodName){
arg = capi.sqlite3_file.resolveToInstance(arg);
if(!arg){
err("sqlite3_io_methods::xClose() was passed a non-sqlite3_file.");
}
return arg;
};
instm._methods = {
// https://sqlite.org/c3ref/io_methods.html
xClose: /*i(P)*/function(f){
/* int (*xClose)(sqlite3_file*) */
log("xClose(",f,")");
if(!(f = instm._requireFileArg(f,'xClose'))) return capi.SQLITE_MISUSE;
f.dispose(/*noting that f has externally-owned memory*/);
return 0;
},
xRead: /*i(Ppij)*/function(f,dest,n,offset){
/* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
log("xRead(",arguments,")");
if(!(f = instm._requireFileArg(f))) return capi.SQLITE_MISUSE;
capi.wasm.heap8().fill(0, dest + offset, n);
return 0;
},
xWrite: /*i(Ppij)*/function(f,dest,n,offset){
/* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
log("xWrite(",arguments,")");
if(!(f=instm._requireFileArg(f,'xWrite'))) return capi.SQLITE_MISUSE;
return 0;
},
xTruncate: /*i(Pj)*/function(f){
/* int (*xTruncate)(sqlite3_file*, sqlite3_int64 size) */
log("xTruncate(",arguments,")");
if(!(f=instm._requireFileArg(f,'xTruncate'))) return capi.SQLITE_MISUSE;
return 0;
},
xSync: /*i(Pi)*/function(f){
/* int (*xSync)(sqlite3_file*, int flags) */
log("xSync(",arguments,")");
if(!(f=instm._requireFileArg(f,'xSync'))) return capi.SQLITE_MISUSE;
return 0;
},
xFileSize: /*i(Pp)*/function(f,pSz){
/* int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize) */
log("xFileSize(",arguments,")");
if(!(f=instm._requireFileArg(f,'xFileSize'))) return capi.SQLITE_MISUSE;
capi.wasm.setMemValue(pSz, 0/*file size*/);
return 0;
},
xLock: /*i(Pi)*/function(f){
/* int (*xLock)(sqlite3_file*, int) */
log("xLock(",arguments,")");
if(!(f=instm._requireFileArg(f,'xLock'))) return capi.SQLITE_MISUSE;
return 0;
},
xUnlock: /*i(Pi)*/function(f){
/* int (*xUnlock)(sqlite3_file*, int) */
log("xUnlock(",arguments,")");
if(!(f=instm._requireFileArg(f,'xUnlock'))) return capi.SQLITE_MISUSE;
return 0;
},
xCheckReservedLock: /*i(Pp)*/function(){
/* int (*xCheckReservedLock)(sqlite3_file*, int *pResOut) */
log("xCheckReservedLock(",arguments,")");
return 0;
},
xFileControl: /*i(Pip)*/function(){
/* int (*xFileControl)(sqlite3_file*, int op, void *pArg) */
log("xFileControl(",arguments,")");
return capi.SQLITE_NOTFOUND;
},
xSectorSize: /*i(P)*/function(){
/* int (*xSectorSize)(sqlite3_file*) */
log("xSectorSize(",arguments,")");
return 0/*???*/;
},
xDeviceCharacteristics:/*i(P)*/function(){
/* int (*xDeviceCharacteristics)(sqlite3_file*) */
log("xDeviceCharacteristics(",arguments,")");
return 0;
}
};
}/*static init*/
iom.$iVersion = 1;
Object.keys(instm._methods).forEach(
(k)=>installMethod(iom, k, instm._methods[k])
);
}/*installIOMethods()*/;
const iom = new sqlite3_io_methods, sfile = new sqlite3_file;
const err = console.error.bind(console);
try {
const IOM = sqlite3_io_methods, S3F = sqlite3_file;
//log("iom proto",iom,iom.constructor.prototype);
//log("sfile",sfile,sfile.constructor.prototype);
T.assert(0===sfile.$pMethods).assert(iom.pointer > 0);
//log("iom",iom);
/** Some of the following tests require that pMethods has a
signature of "P", as opposed to "p". */
sfile.$pMethods = iom;
T.assert(iom === sfile.$pMethods);
sfile.$pMethods = iom.pointer;
T.assert(iom === sfile.$pMethods)
.assert(IOM.resolveToInstance(iom))
.assert(undefined ===IOM.resolveToInstance(sfile))
.mustThrow(()=>IOM.resolveToInstance(0,true))
.assert(S3F.resolveToInstance(sfile.pointer))
.assert(undefined===S3F.resolveToInstance(iom));
T.assert(0===iom.$iVersion);
installIOMethods(iom);
T.assert(1===iom.$iVersion);
//log("iom.__ondispose",iom.__ondispose);
T.assert(Array.isArray(iom.__ondispose)).assert(iom.__ondispose.length>10);
}finally{
iom.dispose();
T.assert(undefined === iom.__ondispose);
}
const dVfs = new sqlite3_vfs(capi.sqlite3_vfs_find(null));
try {
const SB = sqlite3.StructBinder;
T.assert(dVfs instanceof SB.StructType)
.assert(dVfs.pointer)
.assert('sqlite3_vfs' === dVfs.structName)
.assert(!!dVfs.structInfo)
.assert(SB.StructType.hasExternalPointer(dVfs))
.assert(3===dVfs.$iVersion)
.assert('number'===typeof dVfs.$zName)
.assert('number'===typeof dVfs.$xSleep)
.assert(capi.wasm.functionEntry(dVfs.$xOpen))
.assert(dVfs.memberIsString('zName'))
.assert(dVfs.memberIsString('$zName'))
.assert(!dVfs.memberIsString('pAppData'))
.mustThrowMatching(()=>dVfs.memberToJsString('xSleep'),
/Invalid member type signature for C-string/)
.mustThrowMatching(()=>dVfs.memberSignature('nope'), /nope is not a mapped/)
.assert('string' === typeof dVfs.memberToJsString('zName'))
.assert(dVfs.memberToJsString('zName')===dVfs.memberToJsString('$zName'))
;
log("Default VFS: @",dVfs.pointer);
Object.keys(sqlite3_vfs.structInfo.members).forEach(function(mname){
const mk = sqlite3_vfs.memberKey(mname), mbr = sqlite3_vfs.structInfo.members[mname],
addr = dVfs[mk], prefix = 'defaultVfs.'+mname;
if(1===mbr.signature.length){
let sep = '?', val = undefined;
switch(mbr.signature[0]){
// TODO: move this into an accessor, e.g. getPreferredValue(member)
case 'i': case 'j': case 'f': case 'd': sep = '='; val = dVfs[mk]; break
case 'p': case 'P': sep = '@'; val = dVfs[mk]; break;
case 's': sep = '=';
//val = capi.wasm.UTF8ToString(addr);
val = dVfs.memberToJsString(mname);
break;
}
log(prefix, sep, val);
}
else{
log(prefix," = funcptr @",addr, capi.wasm.functionEntry(addr));
}
});
}finally{
dVfs.dispose();
T.assert(undefined===dVfs.pointer);
}
}/*testSqliteStructs()*/;
const testWasmUtil = function(DB,S){
const w = S.capi.wasm;
/**
Maintenance reminder: the rest of this function is part of the
upstream Jaccwabyt tree.
*/
const chr = (x)=>x.charCodeAt(0);
log("heap getters...");
{
const li = [8, 16, 32];
if(w.bigIntEnabled) li.push(64);
for(const n of li){
const bpe = n/8;
const s = w.heapForSize(n,false);
T.assert(bpe===s.BYTES_PER_ELEMENT).
assert(w.heapForSize(s.constructor) === s);
const u = w.heapForSize(n,true);
T.assert(bpe===u.BYTES_PER_ELEMENT).
assert(s!==u).
assert(w.heapForSize(u.constructor) === u);
}
}
log("jstrlen()...");
{
T.assert(3 === w.jstrlen("abc")).assert(4 === w.jstrlen("äbc"));
}
log("jstrcpy()...");
{
const fillChar = 10;
let ua = new Uint8Array(8), rc,
refill = ()=>ua.fill(fillChar);
refill();
rc = w.jstrcpy("hello", ua);
T.assert(6===rc).assert(0===ua[5]).assert(chr('o')===ua[4]);
refill();
ua[5] = chr('!');
rc = w.jstrcpy("HELLO", ua, 0, -1, false);
T.assert(5===rc).assert(chr('!')===ua[5]).assert(chr('O')===ua[4]);
refill();
rc = w.jstrcpy("the end", ua, 4);
//log("rc,ua",rc,ua);
T.assert(4===rc).assert(0===ua[7]).
assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
refill();
rc = w.jstrcpy("the end", ua, 4, -1, false);
T.assert(4===rc).assert(chr(' ')===ua[7]).
assert(chr('e')===ua[6]).assert(chr('t')===ua[4]);
refill();
rc = w.jstrcpy("", ua, 0, 1, true);
//log("rc,ua",rc,ua);
T.assert(1===rc).assert(0===ua[0]);
refill();
rc = w.jstrcpy("x", ua, 0, 1, true);
//log("rc,ua",rc,ua);
T.assert(1===rc).assert(0===ua[0]);
refill();
rc = w.jstrcpy('äbä', ua, 0, 1, true);
T.assert(1===rc, 'Must not write partial multi-byte char.')
.assert(0===ua[0]);
refill();
rc = w.jstrcpy('äbä', ua, 0, 2, true);
T.assert(1===rc, 'Must not write partial multi-byte char.')
.assert(0===ua[0]);
refill();
rc = w.jstrcpy('äbä', ua, 0, 2, false);
T.assert(2===rc).assert(fillChar!==ua[1]).assert(fillChar===ua[2]);
}/*jstrcpy()*/
log("cstrncpy()...");
{
w.scopedAllocPush();
try {
let cStr = w.scopedAllocCString("hello");
const n = w.cstrlen(cStr);
let cpy = w.scopedAlloc(n+10);
let rc = w.cstrncpy(cpy, cStr, n+10);
T.assert(n+1 === rc).
assert("hello" === w.cstringToJs(cpy)).
assert(chr('o') === w.getMemValue(cpy+n-1)).
assert(0 === w.getMemValue(cpy+n));
let cStr2 = w.scopedAllocCString("HI!!!");
rc = w.cstrncpy(cpy, cStr2, 3);
T.assert(3===rc).
assert("HI!lo" === w.cstringToJs(cpy)).
assert(chr('!') === w.getMemValue(cpy+2)).
assert(chr('l') === w.getMemValue(cpy+3));
}finally{
w.scopedAllocPop();
}
}
log("jstrToUintArray()...");
{
let a = w.jstrToUintArray("hello", false);
T.assert(5===a.byteLength).assert(chr('o')===a[4]);
a = w.jstrToUintArray("hello", true);
T.assert(6===a.byteLength).assert(chr('o')===a[4]).assert(0===a[5]);
a = w.jstrToUintArray("äbä", false);
T.assert(5===a.byteLength).assert(chr('b')===a[2]);
a = w.jstrToUintArray("äbä", true);
T.assert(6===a.byteLength).assert(chr('b')===a[2]).assert(0===a[5]);
}
log("allocCString()...");
{
const cstr = w.allocCString("hällo, world");
const n = w.cstrlen(cstr);
T.assert(13 === n)
.assert(0===w.getMemValue(cstr+n))
.assert(chr('d')===w.getMemValue(cstr+n-1));
}
log("scopedAlloc() and friends...");
{
const alloc = w.alloc, dealloc = w.dealloc;
w.alloc = w.dealloc = null;
T.assert(!w.scopedAlloc.level)
.mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
.mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
w.alloc = alloc;
T.mustThrowMatching(()=>w.scopedAllocPush(), /missing alloc/);
w.dealloc = dealloc;
T.mustThrowMatching(()=>w.scopedAllocPop(), /^Invalid state/)
.mustThrowMatching(()=>w.scopedAlloc(1), /^No scopedAllocPush/)
.mustThrowMatching(()=>w.scopedAlloc.level=0, /read-only/);
const asc = w.scopedAllocPush();
let asc2;
try {
const p1 = w.scopedAlloc(16),
p2 = w.scopedAlloc(16);
T.assert(1===w.scopedAlloc.level)
.assert(Number.isFinite(p1))
.assert(Number.isFinite(p2))
.assert(asc[0] === p1)
.assert(asc[1]===p2);
asc2 = w.scopedAllocPush();
const p3 = w.scopedAlloc(16);
T.assert(2===w.scopedAlloc.level)
.assert(Number.isFinite(p3))
.assert(2===asc.length)
.assert(p3===asc2[0]);
const [z1, z2, z3] = w.scopedAllocPtr(3);
T.assert('number'===typeof z1).assert(z2>z1).assert(z3>z2)
.assert(0===w.getMemValue(z1,'i32'), 'allocPtr() must zero the targets')
.assert(0===w.getMemValue(z3,'i32'));
}finally{
// Pop them in "incorrect" order to make sure they behave:
w.scopedAllocPop(asc);
T.assert(0===asc.length);
T.mustThrowMatching(()=>w.scopedAllocPop(asc),
/^Invalid state object/);
if(asc2){
T.assert(2===asc2.length,'Should be p3 and z1');
w.scopedAllocPop(asc2);
T.assert(0===asc2.length);
T.mustThrowMatching(()=>w.scopedAllocPop(asc2),
/^Invalid state object/);
}
}
T.assert(0===w.scopedAlloc.level);
w.scopedAllocCall(function(){
T.assert(1===w.scopedAlloc.level);
const [cstr, n] = w.scopedAllocCString("hello, world", true);
T.assert(12 === n)
.assert(0===w.getMemValue(cstr+n))
.assert(chr('d')===w.getMemValue(cstr+n-1));
});
}/*scopedAlloc()*/
log("xCall()...");
{
const pJson = w.xCall('jaccwabyt_test_ctype_json');
T.assert(Number.isFinite(pJson)).assert(w.cstrlen(pJson)>300);
}
log("xWrap()...");
{
//int jaccwabyt_test_intptr(int * p);
//int64_t jaccwabyt_test_int64_max(void)
//int64_t jaccwabyt_test_int64_min(void)
//int64_t jaccwabyt_test_int64_times2(int64_t x)
//void jaccwabyt_test_int64_minmax(int64_t * min, int64_t *max)
//int64_t jaccwabyt_test_int64ptr(int64_t * p)
//const char * jaccwabyt_test_ctype_json(void)
T.mustThrowMatching(()=>w.xWrap('jaccwabyt_test_ctype_json',null,'i32'),
/requires 0 arg/).
assert(w.xWrap.resultAdapter('i32') instanceof Function).
assert(w.xWrap.argAdapter('i32') instanceof Function);
let fw = w.xWrap('jaccwabyt_test_ctype_json','string');
T.mustThrowMatching(()=>fw(1), /requires 0 arg/);
let rc = fw();
T.assert('string'===typeof rc).assert(rc.length>300);
rc = w.xCallWrapped('jaccwabyt_test_ctype_json','*');
T.assert(rc>0 && Number.isFinite(rc));
rc = w.xCallWrapped('jaccwabyt_test_ctype_json','string');
T.assert('string'===typeof rc).assert(rc.length>300);
fw = w.xWrap('jaccwabyt_test_str_hello', 'string:free',['i32']);
rc = fw(0);
T.assert('hello'===rc);
rc = fw(1);
T.assert(null===rc);
w.xWrap.resultAdapter('thrice', (v)=>3n*BigInt(v));
w.xWrap.argAdapter('twice', (v)=>2n*BigInt(v));
fw = w.xWrap('jaccwabyt_test_int64_times2','thrice','twice');
rc = fw(1);
T.assert(12n===rc);
w.scopedAllocCall(function(){
let pI1 = w.scopedAlloc(8), pI2 = pI1+4;
w.setMemValue(pI1, 0,'*')(pI2, 0, '*');
let f = w.xWrap('jaccwabyt_test_int64_minmax',undefined,['i64*','i64*']);
let r1 = w.getMemValue(pI1, 'i64'), r2 = w.getMemValue(pI2, 'i64');
T.assert(!Number.isSafeInteger(r1)).assert(!Number.isSafeInteger(r2));
});
}
}/*testWasmUtil()*/;
const runTests = function(Module){
//log("Module",Module);
const sqlite3 = Module.sqlite3,
capi = sqlite3.capi,
oo = sqlite3.oo1,
wasm = capi.wasm;
log("Loaded module:",capi.sqlite3_libversion(), capi.sqlite3_sourceid());
log("Build options:",wasm.compileOptionUsed());
capi.sqlite3_web_persistent_dir()/*will install OPFS if available, plus a and non-locking VFS*/;
if(1){
/* Let's grab those last few lines of test coverage for
sqlite3-api.js... */
const rc = wasm.compileOptionUsed(['COMPILER']);
T.assert(1 === rc.COMPILER);
const obj = {COMPILER:undefined};
wasm.compileOptionUsed(obj);
T.assert(1 === obj.COMPILER);
}
log("WASM heap size =",wasm.heap8().length);
//log("capi.wasm.exports.__indirect_function_table",capi.wasm.exports.__indirect_function_table);
const wasmCtypes = wasm.ctype;
//log("wasmCtypes",wasmCtypes);
T.assert(wasmCtypes.structs[0].name==='sqlite3_vfs').
assert(wasmCtypes.structs[0].members.szOsFile.sizeof>=4).
assert(wasmCtypes.structs[1/*sqlite3_io_methods*/
].members.xFileSize.offset>0);
//log(wasmCtypes.structs[0].name,"members",wasmCtypes.structs[0].members);
[ /* Spot-check a handful of constants to make sure they got installed... */
'SQLITE_SCHEMA','SQLITE_NULL','SQLITE_UTF8',
'SQLITE_STATIC', 'SQLITE_DIRECTONLY',
'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE'
].forEach(function(k){
T.assert('number' === typeof capi[k]);
});
[/* Spot-check a few of the WASM API methods. */
'alloc', 'dealloc', 'installFunction'
].forEach(function(k){
T.assert(capi.wasm[k] instanceof Function);
});
const db = new oo.DB(':memory:'), startTime = performance.now();
try {
log("DB filename:",db.filename,db.fileName());
const banner1 = '>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>',
banner2 = '<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<';
[
testWasmUtil, testBasicSanity, testUDF,
testAttach, testIntPtr, testStructStuff,
testSqliteStructs
].forEach((f)=>{
const t = T.counter, n = performance.now();
logHtml(banner1,"Running",f.name+"()...");
f(db, sqlite3, Module);
logHtml(banner2,f.name+"():",T.counter - t,'tests in',(performance.now() - n),"ms");
});
}finally{
db.close();
}
logHtml("Total Test count:",T.counter,"in",(performance.now() - startTime),"ms");
log('capi.wasm.exports',capi.wasm.exports);
};
self.sqlite3TestModule.initSqlite3().then(function(theModule){
self._MODULE = theModule /* this is only to facilitate testing from the console */
runTests(theModule);
});
})();