0
0
mirror of https://github.com/tursodatabase/libsql.git synced 2024-11-27 10:29:00 +00:00
libsql/libsql-sqlite3/ext/wasm/SQLTester/SQLTester.mjs
Piotr Sarna a56ee2f615 ext/wasm: fix SQLTester path
It was moved incorrectly due to the migration of the C API
to libsql-sqlite3/ subdirectory.
2023-11-29 16:53:59 +01:00

1340 lines
36 KiB
JavaScript

/*
** 2023-08-29
**
** 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.
**
*************************************************************************
** This file contains the main application entry pointer for the JS
** implementation of the SQLTester framework.
**
** This version is not well-documented because it's a direct port of
** the Java immplementation, which is documented: in the main SQLite3
** source tree, see ext/jni/src/org/sqlite/jni/tester/SQLite3Tester.java.
*/
import sqlite3ApiInit from '/jswasm/sqlite3.mjs';
const sqlite3 = await sqlite3ApiInit();
const log = (...args)=>{
console.log('SQLTester:',...args);
};
/**
Try to install vfsName as the new default VFS. Once this succeeds
(returns true) then it becomes a no-op on future calls. Throws if
vfs registration as the default VFS fails but has no side effects
if vfsName is not currently registered.
*/
const tryInstallVfs = function f(vfsName){
if(f.vfsName) return false;
const pVfs = sqlite3.capi.sqlite3_vfs_find(vfsName);
if(pVfs){
log("Installing",'"'+vfsName+'"',"as default VFS.");
const rc = sqlite3.capi.sqlite3_vfs_register(pVfs, 1);
if(rc){
sqlite3.SQLite3Error.toss(rc,"While trying to register",vfsName,"vfs.");
}
f.vfsName = vfsName;
}
return !!pVfs;
};
tryInstallVfs.vfsName = undefined;
if( 0 && globalThis.WorkerGlobalScope ){
// Try OPFS storage, if available...
if( 0 && sqlite3.oo1.OpfsDb ){
/* Really slow with these tests */
tryInstallVfs("opfs");
}
if( sqlite3.installOpfsSAHPoolVfs ){
await sqlite3.installOpfsSAHPoolVfs({
clearOnInit: true,
initialCapacity: 15,
name: 'opfs-SQLTester'
}).then(pool=>{
tryInstallVfs(pool.vfsName);
}).catch(e=>{
log("OpfsSAHPool could not load:",e);
});
}
}
const wPost = (function(){
return (('undefined'===typeof WorkerGlobalScope)
? ()=>{}
: (type, payload)=>{
postMessage({type, payload});
});
})();
//log("WorkerGlobalScope",globalThis.WorkerGlobalScope);
// Return a new enum entry value
const newE = ()=>Object.create(null);
const newObj = (props)=>Object.assign(newE(), props);
/**
Modes for how to escape (or not) column values and names from
SQLTester.execSql() to the result buffer output.
*/
const ResultBufferMode = Object.assign(Object.create(null),{
//! Do not append to result buffer
NONE: newE(),
//! Append output escaped.
ESCAPED: newE(),
//! Append output as-is
ASIS: newE()
});
/**
Modes to specify how to emit multi-row output from
SQLTester.execSql() to the result buffer.
*/
const ResultRowMode = newObj({
//! Keep all result rows on one line, space-separated.
ONLINE: newE(),
//! Add a newline between each result row.
NEWLINE: newE()
});
class SQLTesterException extends globalThis.Error {
constructor(testScript, ...args){
if(testScript){
super( [testScript.getOutputPrefix()+": ", ...args].join('') );
}else{
super( args.join('') );
}
this.name = 'SQLTesterException';
}
isFatal() { return false; }
}
SQLTesterException.toss = (...args)=>{
throw new SQLTesterException(...args);
}
class DbException extends SQLTesterException {
constructor(testScript, pDb, rc, closeDb=false){
super(testScript, "DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb));
this.name = 'DbException';
if( closeDb ) sqlite3.capi.sqlite3_close_v2(pDb);
}
isFatal() { return true; }
}
class TestScriptFailed extends SQLTesterException {
constructor(testScript, ...args){
super(testScript,...args);
this.name = 'TestScriptFailed';
}
isFatal() { return true; }
}
class UnknownCommand extends SQLTesterException {
constructor(testScript, cmdName){
super(testScript, cmdName);
this.name = 'UnknownCommand';
}
isFatal() { return true; }
}
class IncompatibleDirective extends SQLTesterException {
constructor(testScript, ...args){
super(testScript,...args);
this.name = 'IncompatibleDirective';
}
}
//! For throwing where an expression is required.
const toss = (errType, ...args)=>{
throw new errType(...args);
};
const __utf8Decoder = new TextDecoder();
const __utf8Encoder = new TextEncoder('utf-8');
//! Workaround for Util.utf8Decode()
const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer)
? function(){} : globalThis.SharedArrayBuffer;
/* Frequently-reused regexes. */
const Rx = newObj({
requiredProperties: / REQUIRED_PROPERTIES:[ \t]*(\S.*)\s*$/,
scriptModuleName: / SCRIPT_MODULE_NAME:[ \t]*(\S+)\s*$/,
mixedModuleName: / ((MIXED_)?MODULE_NAME):[ \t]*(\S+)\s*$/,
command: /^--(([a-z-]+)( .*)?)$/,
//! "Special" characters - we have to escape output if it contains any.
special: /[\x00-\x20\x22\x5c\x7b\x7d]/,
squiggly: /[{}]/
});
const Util = newObj({
toss,
unlink: function(fn){
return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn);
},
argvToString: (list)=>{
const m = [...list];
m.shift() /* strip command name */;
return m.join(" ")
},
utf8Decode: function(arrayBuffer, begin, end){
return __utf8Decoder.decode(
(arrayBuffer.buffer instanceof __SAB)
? arrayBuffer.slice(begin, end)
: arrayBuffer.subarray(begin, end)
);
},
utf8Encode: (str)=>__utf8Encoder.encode(str),
strglob: sqlite3.wasm.xWrap('sqlite3_wasm_SQLTester_strglob','int',
['string','string'])
})/*Util*/;
class Outer {
#lnBuf = [];
#verbosity = 0;
#logger = console.log.bind(console);
constructor(func){
if(func) this.setFunc(func);
}
logger(...args){
if(args.length){
this.#logger = args[0];
return this;
}
return this.#logger;
}
out(...args){
if( this.getOutputPrefix && !this.#lnBuf.length ){
this.#lnBuf.push(this.getOutputPrefix());
}
this.#lnBuf.push(...args);
return this;
}
#outlnImpl(vLevel, ...args){
if( this.getOutputPrefix && !this.#lnBuf.length ){
this.#lnBuf.push(this.getOutputPrefix());
}
this.#lnBuf.push(...args,'\n');
const msg = this.#lnBuf.join('');
this.#lnBuf.length = 0;
this.#logger(msg);
return this;
}
outln(...args){
return this.#outlnImpl(0,...args);
}
outputPrefix(){
if( 0==arguments.length ){
return (this.getOutputPrefix
? (this.getOutputPrefix() ?? '') : '');
}else{
this.getOutputPrefix = arguments[0];
return this;
}
}
static #verboseLabel = ["🔈",/*"🔉",*/"🔊","📢"];
verboseN(lvl, args){
if( this.#verbosity>=lvl ){
this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
}
}
verbose1(...args){ return this.verboseN(1,args); }
verbose2(...args){ return this.verboseN(2,args); }
verbose3(...args){ return this.verboseN(3,args); }
verbosity(){
const rc = this.#verbosity;
if(arguments.length) this.#verbosity = +arguments[0];
return rc;
}
}/*Outer*/
class SQLTester {
//! Console output utility.
#outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
//! List of input scripts.
#aScripts = [];
//! Test input buffer.
#inputBuffer = [];
//! Test result buffer.
#resultBuffer = [];
//! Output representation of SQL NULL.
#nullView;
metrics = newObj({
//! Total tests run
nTotalTest: 0,
//! Total test script files run
nTestFile: 0,
//! Test-case count for to the current TestScript
nTest: 0,
//! Names of scripts which were aborted.
failedScripts: []
});
#emitColNames = false;
//! True to keep going regardless of how a test fails.
#keepGoing = false;
#db = newObj({
//! The list of available db handles.
list: new Array(7),
//! Index into this.list of the current db.
iCurrentDb: 0,
//! Name of the default db, re-created for each script.
initialDbName: "test.db",
//! Buffer for REQUIRED_PROPERTIES pragmas.
initSql: ['select 1;'],
//! (sqlite3*) to the current db.
currentDb: function(){
return this.list[this.iCurrentDb];
}
});
constructor(){
this.reset();
}
outln(...args){ return this.#outer.outln(...args); }
out(...args){ return this.#outer.out(...args); }
outer(...args){
if(args.length){
this.#outer = args[0];
return this;
}
return this.#outer;
}
verbose1(...args){ return this.#outer.verboseN(1,args); }
verbose2(...args){ return this.#outer.verboseN(2,args); }
verbose3(...args){ return this.#outer.verboseN(3,args); }
verbosity(...args){
const rc = this.#outer.verbosity(...args);
return args.length ? this : rc;
}
setLogger(func){
this.#outer.logger(func);
return this;
}
incrementTestCounter(){
++this.metrics.nTotalTest;
++this.metrics.nTest;
}
reset(){
this.clearInputBuffer();
this.clearResultBuffer();
this.#clearBuffer(this.#db.initSql);
this.closeAllDbs();
this.metrics.nTest = 0;
this.#nullView = "nil";
this.emitColNames = false;
this.#db.iCurrentDb = 0;
//this.#db.initSql.push("SELECT 1;");
}
appendInput(line, addNL){
this.#inputBuffer.push(line);
if( addNL ) this.#inputBuffer.push('\n');
}
appendResult(line, addNL){
this.#resultBuffer.push(line);
if( addNL ) this.#resultBuffer.push('\n');
}
appendDbInitSql(sql){
this.#db.initSql.push(sql);
if( this.currentDb() ){
this.execSql(null, true, ResultBufferMode.NONE, null, sql);
}
}
#runInitSql(pDb){
let rc = 0;
for(const sql of this.#db.initSql){
this.#outer.verbose2("RUNNING DB INIT CODE: ",sql);
rc = this.execSql(pDb, false, ResultBufferMode.NONE, null, sql);
if( rc ) break;
}
return rc;
}
#clearBuffer(buffer){
buffer.length = 0;
return buffer;
}
clearInputBuffer(){ return this.#clearBuffer(this.#inputBuffer); }
clearResultBuffer(){return this.#clearBuffer(this.#resultBuffer); }
getInputText(){ return this.#inputBuffer.join(''); }
getResultText(){ return this.#resultBuffer.join(''); }
#takeBuffer(buffer){
const s = buffer.join('');
buffer.length = 0;
return s;
}
takeInputBuffer(){
return this.#takeBuffer(this.#inputBuffer);
}
takeResultBuffer(){
return this.#takeBuffer(this.#resultBuffer);
}
nullValue(){
return (0==arguments.length)
? this.#nullView
: (this.#nullView = ''+arguments[0]);
}
outputColumnNames(){
return (0==arguments.length)
? this.#emitColNames
: (this.#emitColNames = !!arguments[0]);
}
currentDbId(){
return (0==arguments.length)
? this.#db.iCurrentDb
: (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
}
#affirmDbId(id){
if(id<0 || id>=this.#db.list.length){
toss(SQLTesterException, "Database index ",id," is out of range.");
}
return this;
}
currentDb(...args){
if( 0!=args.length ){
this.#affirmDbId(id).#db.iCurrentDb = id;
}
return this.#db.currentDb();
}
getDbById(id){
return this.#affirmDbId(id).#db.list[id];
}
getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
closeDb(id) {
if( 0==arguments.length ){
id = this.#db.iCurrentDb;
}
const pDb = this.#affirmDbId(id).#db.list[id];
if( pDb ){
sqlite3.capi.sqlite3_close_v2(pDb);
this.#db.list[id] = null;
}
}
closeAllDbs(){
for(let i = 0; i<this.#db.list.length; ++i){
if(this.#db.list[i]){
sqlite3.capi.sqlite3_close_v2(this.#db.list[i]);
this.#db.list[i] = null;
}
}
this.#db.iCurrentDb = 0;
}
openDb(name, createIfNeeded){
if( 3===arguments.length ){
const slot = arguments[0];
this.#affirmDbId(slot).#db.iCurrentDb = slot;
name = arguments[1];
createIfNeeded = arguments[2];
}
this.closeDb();
const capi = sqlite3.capi, wasm = sqlite3.wasm;
let pDb = 0;
let flags = capi.SQLITE_OPEN_READWRITE;
if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
try{
let rc;
wasm.pstack.call(function(){
let ppOut = wasm.pstack.allocPtr();
rc = sqlite3.capi.sqlite3_open_v2(name, ppOut, flags, null);
pDb = wasm.peekPtr(ppOut);
});
let sql;
if( 0==rc && this.#db.initSql.length > 0){
rc = this.#runInitSql(pDb);
}
if( 0!=rc ){
sqlite3.SQLite3Error.toss(
rc,
"sqlite3 result code",rc+":",
(pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
: sqlite3.capi.sqlite3_errstr(rc))
);
}
return this.#db.list[this.#db.iCurrentDb] = pDb;
}catch(e){
sqlite3.capi.sqlite3_close_v2(pDb);
throw e;
}
}
addTestScript(ts){
if( 2===arguments.length ){
ts = new TestScript(arguments[0], arguments[1]);
}else if(ts instanceof Uint8Array){
ts = new TestScript('<unnamed>', ts);
}else if('string' === typeof arguments[1]){
ts = new TestScript('<unnamed>', Util.utf8Encode(arguments[1]));
}
if( !(ts instanceof TestScript) ){
Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
}
this.#aScripts.push(ts);
return this;
}
runTests(){
const tStart = (new Date()).getTime();
let isVerbose = this.verbosity();
this.metrics.failedScripts.length = 0;
this.metrics.nTotalTest = 0;
this.metrics.nTestFile = 0;
for(const ts of this.#aScripts){
this.reset();
++this.metrics.nTestFile;
let threw = false;
const timeStart = (new Date()).getTime();
let msgTail = '';
try{
ts.run(this);
}catch(e){
if(e instanceof SQLTesterException){
threw = true;
this.outln("🔥EXCEPTION: ",e);
this.metrics.failedScripts.push({script: ts.filename(), message:e.toString()});
if( this.#keepGoing ){
this.outln("Continuing anyway because of the keep-going option.");
}else if( e.isFatal() ){
throw e;
}
}else{
throw e;
}
}finally{
const timeEnd = (new Date()).getTime();
this.out("🏁", (threw ? "❌" : "✅"), " ",
this.metrics.nTest, " test(s) in ",
(timeEnd-timeStart),"ms. ");
const mod = ts.moduleName();
if( mod ){
this.out( "[",mod,"] " );
}
this.outln(ts.filename());
}
}
const tEnd = (new Date()).getTime();
Util.unlink(this.#db.initialDbName);
this.outln("Took ",(tEnd-tStart),"ms. Test count = ",
this.metrics.nTotalTest,", script count = ",
this.#aScripts.length,(
this.metrics.failedScripts.length
? ", failed scripts = "+this.metrics.failedScripts.length
: ""
)
);
return this;
}
#setupInitialDb(){
if( !this.#db.list[0] ){
Util.unlink(this.#db.initialDbName);
this.openDb(0, this.#db.initialDbName, true);
}else{
this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
"triggered while it is opened.");
}
}
#escapeSqlValue(v){
if( !v ) return "{}";
if( !Rx.special.test(v) ){
return v /* no escaping needed */;
}
if( !Rx.squiggly.test(v) ){
return "{"+v+"}";
}
const sb = ["\""];
const n = v.length;
for(let i = 0; i < n; ++i){
const ch = v.charAt(i);
switch(ch){
case '\\': sb.push("\\\\"); break;
case '"': sb.push("\\\""); break;
default:{
//verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
const ccode = ch.charCodeAt(i);
if( ccode < 32 ) sb.push('\\',ccode.toString(8),'o');
else sb.push(ch);
break;
}
}
}
sb.append("\"");
return sb.join('');
}
#appendDbErr(pDb, sb, rc){
sb.push(sqlite3.capi.sqlite3_js_rc_str(rc), ' ');
const msg = this.#escapeSqlValue(sqlite3.capi.sqlite3_errmsg(pDb));
if( '{' === msg.charAt(0) ){
sb.push(msg);
}else{
sb.push('{', msg, '}');
}
}
#checkDbRc(pDb,rc){
sqlite3.oo1.DB.checkRc(pDb, rc);
}
execSql(pDb, throwOnError, appendMode, rowMode, sql){
if( !pDb && !this.#db.list[0] ){
this.#setupInitialDb();
}
if( !pDb ) pDb = this.#db.currentDb();
const wasm = sqlite3.wasm, capi = sqlite3.capi;
sql = (sql instanceof Uint8Array)
? sql
: Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
const self = this;
const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
let rc = 0;
wasm.scopedAllocCall(function(){
let sqlByteLen = sql.byteLength;
const ppStmt = wasm.scopedAlloc(
/* output (sqlite3_stmt**) arg and pzTail */
(2 * wasm.ptrSizeof) + (sqlByteLen + 1/* SQL + NUL */)
);
const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
let pSql = pzTail + wasm.ptrSizeof;
const pSqlEnd = pSql + sqlByteLen;
wasm.heap8().set(sql, pSql);
wasm.poke8(pSql + sqlByteLen, 0/*NUL terminator*/);
let pos = 0, n = 1, spacing = 0;
while( pSql && wasm.peek8(pSql) ){
wasm.pokePtr([ppStmt, pzTail], 0);
rc = capi.sqlite3_prepare_v3(
pDb, pSql, sqlByteLen, 0, ppStmt, pzTail
);
if( 0!==rc ){
if(throwOnError){
throw new DbException(self, pDb, rc);
}else if( sb ){
self.#appendDbErr(pDb, sb, rc);
}
break;
}
const pStmt = wasm.peekPtr(ppStmt);
pSql = wasm.peekPtr(pzTail);
sqlByteLen = pSqlEnd - pSql;
if(!pStmt) continue /* only whitespace or comments */;
if( sb ){
const nCol = capi.sqlite3_column_count(pStmt);
let colName, val;
while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {
for( let i=0; i < nCol; ++i ){
if( spacing++ > 0 ) sb.push(' ');
if( self.#emitColNames ){
colName = capi.sqlite3_column_name(pStmt, i);
switch(appendMode){
case ResultBufferMode.ASIS: sb.push( colName ); break;
case ResultBufferMode.ESCAPED:
sb.push( self.#escapeSqlValue(colName) );
break;
default:
self.toss("Unhandled ResultBufferMode.");
}
sb.push(' ');
}
val = capi.sqlite3_column_text(pStmt, i);
if( null===val ){
sb.push( self.#nullView );
continue;
}
switch(appendMode){
case ResultBufferMode.ASIS: sb.push( val ); break;
case ResultBufferMode.ESCAPED:
sb.push( self.#escapeSqlValue(val) );
break;
}
}/* column loop */
}/* row loop */
if( ResultRowMode.NEWLINE === rowMode ){
spacing = 0;
sb.push('\n');
}
}else{ // no output but possibly other side effects
while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
}
capi.sqlite3_finalize(pStmt);
if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
else if( rc!=0 ){
if( sb ){
self.#appendDbErr(db, sb, rc);
}
break;
}
}/* SQL script loop */;
})/*scopedAllocCall()*/;
return rc;
}
}/*SQLTester*/
class Command {
constructor(){
}
process(sqlTester,testScript,argv){
SQLTesterException.toss("process() must be overridden");
}
argcCheck(testScript,argv,min,max){
const argc = argv.length-1;
if(argc<min || (max>=0 && argc>max)){
if( min==max ){
testScript.toss(argv[0]," requires exactly ",min," argument(s)");
}else if(max>0){
testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
}else{
testScript.toss(argv[0]," requires at least ",min," arguments.");
}
}
}
}
class Cursor {
src;
sb = [];
pos = 0;
//! Current line number. Starts at 0 for internal reasons and will
// line up with 1-based reality once parsing starts.
lineNo = 0 /* yes, zero */;
//! Putback value for this.pos.
putbackPos = 0;
//! Putback line number
putbackLineNo = 0;
//! Peeked-to pos, used by peekLine() and consumePeeked().
peekedPos = 0;
//! Peeked-to line number.
peekedLineNo = 0;
constructor(){
}
//! Restore parsing state to the start of the stream.
rewind(){
this.sb.length = this.pos = this.lineNo
= this.putbackPos = this.putbackLineNo
= this.peekedPos = this.peekedLineNo = 0;
}
}
class TestScript {
#cursor = new Cursor();
#moduleName = null;
#filename = null;
#testCaseName = null;
#outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
constructor(...args){
let content, filename;
if( 2 == args.length ){
filename = args[0];
content = args[1];
}else if( 1 == args.length ){
if(args[0] instanceof Object){
const o = args[0];
filename = o.name;
content = o.content;
}else{
content = args[0];
}
}
if(!(content instanceof Uint8Array)){
if('string' === typeof content){
content = Util.utf8Encode(content);
}else if((content instanceof ArrayBuffer)
||(content instanceof Array)){
content = new Uint8Array(content);
}else{
toss(Error, "Invalid content type for TestScript constructor.");
}
}
this.#filename = filename;
this.#cursor.src = content;
}
moduleName(){
return (0==arguments.length)
? this.#moduleName : (this.#moduleName = arguments[0]);
}
testCaseName(){
return (0==arguments.length)
? this.#testCaseName : (this.#testCaseName = arguments[0]);
}
filename(){
return (0==arguments.length)
? this.#filename : (this.#filename = arguments[0]);
}
getOutputPrefix() {
let rc = "["+(this.#moduleName || '<unnamed>')+"]";
if( this.#testCaseName ) rc += "["+this.#testCaseName+"]";
if( this.#filename ) rc += '['+this.#filename+']';
return rc + " line "+ this.#cursor.lineNo;
}
reset(){
this.#testCaseName = null;
this.#cursor.rewind();
return this;
}
toss(...args){
throw new TestScriptFailed(this,...args);
}
verbose1(...args){ return this.#outer.verboseN(1,args); }
verbose2(...args){ return this.#outer.verboseN(2,args); }
verbose3(...args){ return this.#outer.verboseN(3,args); }
verbosity(...args){
const rc = this.#outer.verbosity(...args);
return args.length ? this : rc;
}
#checkRequiredProperties(tester, props){
if(true) return false;
let nOk = 0;
for(const rp of props){
this.verbose2("REQUIRED_PROPERTIES: ",rp);
switch(rp){
case "RECURSIVE_TRIGGERS":
tester.appendDbInitSql("pragma recursive_triggers=on;");
++nOk;
break;
case "TEMPSTORE_FILE":
/* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
which we just happen to know is the case */
tester.appendDbInitSql("pragma temp_store=1;");
++nOk;
break;
case "TEMPSTORE_MEM":
/* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
which we just happen to know is the case */
tester.appendDbInitSql("pragma temp_store=0;");
++nOk;
break;
case "AUTOVACUUM":
tester.appendDbInitSql("pragma auto_vacuum=full;");
++nOk;
break;
case "INCRVACUUM":
tester.appendDbInitSql("pragma auto_vacuum=incremental;");
++nOk;
default:
break;
}
}
return props.length == nOk;
}
#checkForDirective(tester,line){
if(line.startsWith("#")){
throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
}else if(line.startsWith("---")){
throw new IncompatibleDirective(this, "triple-dash: ",line);
}
let m = Rx.scriptModuleName.exec(line);
if( m ){
this.#moduleName = m[1];
return;
}
m = Rx.requiredProperties.exec(line);
if( m ){
const rp = m[1];
if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
}
}
m = Rx.mixedModuleName.exec(line);
if( m ){
throw new IncompatibleDirective(this, m[1]+": "+m[3]);
}
if( line.indexOf("\n|")>=0 ){
throw new IncompatibleDirective(this, "newline-pipe combination.");
}
}
#getCommandArgv(line){
const m = Rx.command.exec(line);
return m ? m[1].trim().split(/\s+/) : null;
}
#isCommandLine(line, checkForImpl){
let m = Rx.command.exec(line);
if( m && checkForImpl ){
m = !!CommandDispatcher.getCommandByName(m[2]);
}
return !!m;
}
fetchCommandBody(tester){
const sb = [];
let line;
while( (null !== (line = this.peekLine())) ){
this.#checkForDirective(tester, line);
if( this.#isCommandLine(line, true) ) break;
sb.push(line,"\n");
this.consumePeeked();
}
line = sb.join('');
return !!line.trim() ? line : null;
}
run(tester){
this.reset();
this.#outer.verbosity( tester.verbosity() );
this.#outer.logger( tester.outer().logger() );
let line, directive, argv = [];
while( null != (line = this.getLine()) ){
this.verbose3("run() input line: ",line);
this.#checkForDirective(tester, line);
argv = this.#getCommandArgv(line);
if( argv ){
this.#processCommand(tester, argv);
continue;
}
tester.appendInput(line,true);
}
return true;
}
#processCommand(tester, argv){
this.verbose2("processCommand(): ",argv[0], " ", Util.argvToString(argv));
if(this.#outer.verbosity()>1){
const input = tester.getInputText();
this.verbose3("processCommand() input buffer = ",input);
}
CommandDispatcher.dispatch(tester, this, argv);
}
getLine(){
const cur = this.#cursor;
if( cur.pos==cur.src.byteLength ){
return null/*EOF*/;
}
cur.putbackPos = cur.pos;
cur.putbackLineNo = cur.lineNo;
cur.sb.length = 0;
let b = 0, prevB = 0, i = cur.pos;
let doBreak = false;
let nChar = 0 /* number of bytes in the aChar char */;
const end = cur.src.byteLength;
for(; i < end && !doBreak; ++i){
b = cur.src[i];
switch( b ){
case 13/*CR*/: continue;
case 10/*NL*/:
++cur.lineNo;
if(cur.sb.length>0) doBreak = true;
// Else it's an empty string
break;
default:{
/* Multi-byte chars need to be gathered up and appended at
one time so that we can get them as string objects. */
nChar = 1;
switch( b & 0xF0 ){
case 0xC0: nChar = 2; break;
case 0xE0: nChar = 3; break;
case 0xF0: nChar = 4; break;
default:
if( b > 127 ) this.toss("Invalid character (#"+b+").");
break;
}
if( 1==nChar ){
cur.sb.push(String.fromCharCode(b));
}else{
const aChar = [] /* multi-byte char buffer */;
for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
cur.sb.push(
Util.utf8Decode( new Uint8Array(aChar) )
);
i += nChar-1;
}
break;
}
}
}
cur.pos = i;
const rv = cur.sb.join('');
if( i==cur.src.byteLength && 0==rv.length ){
return null /* EOF */;
}
return rv;
}/*getLine()*/
/**
Fetches the next line then resets the cursor to its pre-call
state. consumePeeked() can be used to consume this peeked line
without having to re-parse it.
*/
peekLine(){
const cur = this.#cursor;
const oldPos = cur.pos;
const oldPB = cur.putbackPos;
const oldPBL = cur.putbackLineNo;
const oldLine = cur.lineNo;
try {
return this.getLine();
}finally{
cur.peekedPos = cur.pos;
cur.peekedLineNo = cur.lineNo;
cur.pos = oldPos;
cur.lineNo = oldLine;
cur.putbackPos = oldPB;
cur.putbackLineNo = oldPBL;
}
}
/**
Only valid after calling peekLine() and before calling getLine().
This places the cursor to the position it would have been at had
the peekLine() had been fetched with getLine().
*/
consumePeeked(){
const cur = this.#cursor;
cur.pos = cur.peekedPos;
cur.lineNo = cur.peekedLineNo;
}
/**
Restores the cursor to the position it had before the previous
call to getLine().
*/
putbackLine(){
const cur = this.#cursor;
cur.pos = cur.putbackPos;
cur.lineNo = cur.putbackLineNo;
}
}/*TestScript*/;
//! --close command
class CloseDbCommand extends Command {
process(t, ts, argv){
this.argcCheck(ts,argv,0,1);
let id;
if(argv.length>1){
const arg = argv[1];
if( "all" === arg ){
t.closeAllDbs();
return;
}
else{
id = parseInt(arg);
}
}else{
id = t.currentDbId();
}
t.closeDb(id);
}
}
//! --column-names command
class ColumnNamesCommand extends Command {
process( st, ts, argv ){
this.argcCheck(ts,argv,1);
st.outputColumnNames( !!parseInt(argv[1]) );
}
}
//! --db command
class DbCommand extends Command {
process(t, ts, argv){
this.argcCheck(ts,argv,1);
t.currentDbId( parseInt(argv[1]) );
}
}
//! --glob command
class GlobCommand extends Command {
#negate = false;
constructor(negate=false){
super();
this.#negate = negate;
}
process(t, ts, argv){
this.argcCheck(ts,argv,1,-1);
t.incrementTestCounter();
const sql = t.takeInputBuffer();
let rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
ResultRowMode.ONELINE, sql);
const result = t.getResultText();
const sArgs = Util.argvToString(argv);
//t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
const glob = Util.argvToString(argv);
rc = Util.strglob(glob, result);
if( (this.#negate && 0===rc) || (!this.#negate && 0!==rc) ){
ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
}
}
}
//! --notglob command
class NotGlobCommand extends GlobCommand {
constructor(){super(true);}
}
//! --open command
class OpenDbCommand extends Command {
#createIfNeeded = false;
constructor(createIfNeeded=false){
super();
this.#createIfNeeded = createIfNeeded;
}
process(t, ts, argv){
this.argcCheck(ts,argv,1);
t.openDb(argv[1], this.#createIfNeeded);
}
}
//! --new command
class NewDbCommand extends OpenDbCommand {
constructor(){ super(true); }
}
//! Placeholder dummy/no-op commands
class NoopCommand extends Command {
process(t, ts, argv){}
}
//! --null command
class NullCommand extends Command {
process(st, ts, argv){
this.argcCheck(ts,argv,1);
st.nullValue( argv[1] );
}
}
//! --print command
class PrintCommand extends Command {
process(st, ts, argv){
st.out(ts.getOutputPrefix(),': ');
if( 1==argv.length ){
st.out( st.getInputText() );
}else{
st.outln( Util.argvToString(argv) );
}
}
}
//! --result command
class ResultCommand extends Command {
#bufferMode;
constructor(resultBufferMode = ResultBufferMode.ESCAPED){
super();
this.#bufferMode = resultBufferMode;
}
process(t, ts, argv){
this.argcCheck(ts,argv,0,-1);
t.incrementTestCounter();
const sql = t.takeInputBuffer();
//ts.verbose2(argv[0]," SQL =\n",sql);
t.execSql(null, false, this.#bufferMode, ResultRowMode.ONELINE, sql);
const result = t.getResultText().trim();
const sArgs = argv.length>1 ? Util.argvToString(argv) : "";
if( result !== sArgs ){
t.outln(argv[0]," FAILED comparison. Result buffer:\n",
result,"\nExpected result:\n",sArgs);
ts.toss(argv[0]+" comparison failed.");
}
}
}
//! --json command
class JsonCommand extends ResultCommand {
constructor(){ super(ResultBufferMode.ASIS); }
}
//! --run command
class RunCommand extends Command {
process(t, ts, argv){
this.argcCheck(ts,argv,0,1);
const pDb = (1==argv.length)
? t.currentDb() : t.getDbById( parseInt(argv[1]) );
const sql = t.takeInputBuffer();
const rc = t.execSql(pDb, false, ResultBufferMode.NONE,
ResultRowMode.ONELINE, sql);
if( 0!==rc && t.verbosity()>0 ){
const msg = sqlite3.capi.sqlite3_errmsg(pDb);
ts.verbose2(argv[0]," non-fatal command error #",rc,": ",
msg,"\nfor SQL:\n",sql);
}
}
}
//! --tableresult command
class TableResultCommand extends Command {
#jsonMode;
constructor(jsonMode=false){
super();
this.#jsonMode = jsonMode;
}
process(t, ts, argv){
this.argcCheck(ts,argv,0);
t.incrementTestCounter();
let body = ts.fetchCommandBody(t);
if( null===body ) ts.toss("Missing ",argv[0]," body.");
body = body.trim();
if( !body.endsWith("\n--end") ){
ts.toss(argv[0], " must be terminated with --end\\n");
}else{
body = body.substring(0, body.length-6);
}
const globs = body.split(/\s*\n\s*/);
if( globs.length < 1 ){
ts.toss(argv[0], " requires 1 or more ",
(this.#jsonMode ? "json snippets" : "globs"),".");
}
const sql = t.takeInputBuffer();
t.execSql(null, true,
this.#jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
ResultRowMode.NEWLINE, sql);
const rbuf = t.getResultText().trim();
const res = rbuf.split(/\r?\n/);
if( res.length !== globs.length ){
ts.toss(argv[0], " failure: input has ", res.length,
" row(s) but expecting ",globs.length);
}
for(let i = 0; i < res.length; ++i){
const glob = globs[i].replaceAll(/\s+/g," ").trim();
//ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
if( this.#jsonMode ){
if( glob!==res[i] ){
ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
res[i],">>");
}
}else if( 0!=Util.strglob(glob, res[i]) ){
ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
}
}
}
}
//! --json-block command
class JsonBlockCommand extends TableResultCommand {
constructor(){ super(true); }
}
//! --testcase command
class TestCaseCommand extends Command {
process(tester, script, argv){
this.argcCheck(script, argv,1);
script.testCaseName(argv[1]);
tester.clearResultBuffer();
tester.clearInputBuffer();
}
}
//! --verbosity command
class VerbosityCommand extends Command {
process(t, ts, argv){
this.argcCheck(ts,argv,1);
ts.verbosity( parseInt(argv[1]) );
}
}
class CommandDispatcher {
static map = newObj();
static getCommandByName(name){
let rv = CommandDispatcher.map[name];
if( rv ) return rv;
switch(name){
case "close": rv = new CloseDbCommand(); break;
case "column-names": rv = new ColumnNamesCommand(); break;
case "db": rv = new DbCommand(); break;
case "glob": rv = new GlobCommand(); break;
case "json": rv = new JsonCommand(); break;
case "json-block": rv = new JsonBlockCommand(); break;
case "new": rv = new NewDbCommand(); break;
case "notglob": rv = new NotGlobCommand(); break;
case "null": rv = new NullCommand(); break;
case "oom": rv = new NoopCommand(); break;
case "open": rv = new OpenDbCommand(); break;
case "print": rv = new PrintCommand(); break;
case "result": rv = new ResultCommand(); break;
case "run": rv = new RunCommand(); break;
case "tableresult": rv = new TableResultCommand(); break;
case "testcase": rv = new TestCaseCommand(); break;
case "verbosity": rv = new VerbosityCommand(); break;
}
if( rv ){
CommandDispatcher.map[name] = rv;
}
return rv;
}
static dispatch(tester, testScript, argv){
const cmd = CommandDispatcher.getCommandByName(argv[0]);
if( !cmd ){
toss(UnknownCommand,testScript,argv[0]);
}
cmd.process(tester, testScript, argv);
}
}/*CommandDispatcher*/
const namespace = newObj({
Command,
DbException,
IncompatibleDirective,
Outer,
SQLTester,
SQLTesterException,
TestScript,
TestScriptFailed,
UnknownCommand,
Util,
sqlite3
});
export {namespace as default};