/* ** 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 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('', ts); }else if('string' === typeof arguments[1]){ ts = new TestScript('', 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=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 || '')+"]"; 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};