| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import sqlite3ApiInit from '/jswasm/sqlite3.mjs'; |
| |
|
| | const sqlite3 = await sqlite3ApiInit(); |
| |
|
| | const log = (...args)=>{ |
| | console.log('SQLTester:',...args); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 ){ |
| | |
| | if( 1 && sqlite3.oo1.OpfsDb ){ |
| | |
| | tryInstallVfs("opfs"); |
| | }else 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 newE = ()=>Object.create(null); |
| |
|
| | const newObj = (props)=>Object.assign(newE(), props); |
| |
|
| | |
| | |
| | |
| | |
| | const ResultBufferMode = Object.assign(Object.create(null),{ |
| | |
| | NONE: newE(), |
| | |
| | ESCAPED: newE(), |
| | |
| | ASIS: newE() |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | const ResultRowMode = newObj({ |
| | |
| | ONLINE: newE(), |
| | |
| | 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'; |
| | } |
| | } |
| |
|
| | |
| | const toss = (errType, ...args)=>{ |
| | throw new errType(...args); |
| | }; |
| |
|
| | const __utf8Decoder = new TextDecoder(); |
| | const __utf8Encoder = new TextEncoder('utf-8'); |
| | |
| | const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer) |
| | ? function(){} : globalThis.SharedArrayBuffer; |
| |
|
| |
|
| | |
| | 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: /[\x00-\x20\x22\x5c\x7b\x7d]/, |
| | squiggly: /[{}]/ |
| | }); |
| |
|
| |
|
| |
|
| | const Util = newObj({ |
| | toss, |
| |
|
| | unlink: function f(fn){ |
| | if(!f.unlink){ |
| | f.unlink = sqlite3.wasm.xWrap('sqlite3__wasm_vfs_unlink','int', |
| | ['*','string']); |
| | } |
| | return 0==f.unlink(0,fn); |
| | }, |
| |
|
| | argvToString: (list)=>{ |
| | const m = [...list]; |
| | m.shift() ; |
| | 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']) |
| | }); |
| |
|
| | |
| | |
| | |
| | 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; |
| | } |
| |
|
| | } |
| |
|
| | class SQLTester { |
| |
|
| | |
| | #outer = new Outer().outputPrefix( ()=>'SQLTester: ' ); |
| | |
| | #aScripts = []; |
| | |
| | #inputBuffer = []; |
| | |
| | #resultBuffer = []; |
| | |
| | #nullView; |
| | metrics = newObj({ |
| | |
| | nTotalTest: 0, |
| | |
| | nTestFile: 0, |
| | |
| | nTest: 0, |
| | |
| | failedScripts: [] |
| | }); |
| | #emitColNames = false; |
| | |
| | #keepGoing = false; |
| | #db = newObj({ |
| | |
| | list: new Array(7), |
| | |
| | iCurrentDb: 0, |
| | |
| | initialDbName: "test.db", |
| | |
| | initSql: ['select 1;'], |
| | |
| | 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; |
| | |
| | } |
| |
|
| | 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(){ |
| | this.outln("SQLite version ", sqlite3.capi.sqlite3_libversion()," with ", |
| | sqlite3.wasm.ptr.size, "-byte WASM pointers"); |
| | 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 ; |
| | } |
| | 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:{ |
| | |
| | const ccode = ch.charCodeAt(i); |
| | if( ccode < 32 ) sb.push('\\',ccode.toString(8),'o'); |
| | else sb.push(ch); |
| | break; |
| | } |
| | } |
| | } |
| | sb.push("\""); |
| | 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 = Number(sql.byteLength); |
| | const ppStmt = wasm.scopedAlloc( |
| | (2 * wasm.ptr.size) |
| | + (sqlByteLen + 1) |
| | ); |
| | const pzTail = wasm.ptr.add(ppStmt, wasm.ptr.size) ; |
| | let pSql = wasm.ptr.add(pzTail, wasm.ptr.size); |
| | const pSqlEnd = wasm.ptr.add(pSql, sqlByteLen); |
| | wasm.heap8().set(sql, Number(pSql)); |
| | wasm.poke8(pSqlEnd, 0); |
| | 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 = Number(pSqlEnd - pSql); |
| | if(!pStmt) continue ; |
| | 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; |
| | } |
| | } |
| | if( ResultRowMode.NEWLINE === rowMode ){ |
| | spacing = 0; |
| | sb.push('\n'); |
| | } |
| | } |
| | }else{ |
| | 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(pDb, sb, rc); |
| | } |
| | break; |
| | } |
| | }; |
| | }); |
| | return rc; |
| | } |
| |
|
| | } |
| |
|
| | 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; |
| | |
| | |
| | lineNo = 0 ; |
| | |
| | putbackPos = 0; |
| | |
| | putbackLineNo = 0; |
| | |
| | peekedPos = 0; |
| | |
| | peekedLineNo = 0; |
| |
|
| | constructor(){ |
| | } |
| |
|
| | |
| | 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": |
| | |
| | |
| | tester.appendDbInitSql("pragma temp_store=1;"); |
| | ++nOk; |
| | break; |
| | case "TEMPSTORE_MEM": |
| | |
| | |
| | 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; |
| | } |
| | 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 ; |
| | const end = cur.src.byteLength; |
| | for(; i < end && !doBreak; ++i){ |
| | b = cur.src[i]; |
| | switch( b ){ |
| | case 13: continue; |
| | case 10: |
| | ++cur.lineNo; |
| | if(cur.sb.length>0) doBreak = true; |
| | |
| | break; |
| | default:{ |
| | |
| | |
| | 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 = [] ; |
| | 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 ; |
| | } |
| | return rv; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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; |
| | } |
| | } |
| |
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | consumePeeked(){ |
| | const cur = this.#cursor; |
| | cur.pos = cur.peekedPos; |
| | cur.lineNo = cur.peekedLineNo; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | putbackLine(){ |
| | const cur = this.#cursor; |
| | cur.pos = cur.putbackPos; |
| | cur.lineNo = cur.putbackLineNo; |
| | } |
| |
|
| | }; |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | class ColumnNamesCommand extends Command { |
| | process( st, ts, argv ){ |
| | this.argcCheck(ts,argv,1); |
| | st.outputColumnNames( !!parseInt(argv[1]) ); |
| | } |
| | } |
| |
|
| | |
| | class DbCommand extends Command { |
| | process(t, ts, argv){ |
| | this.argcCheck(ts,argv,1); |
| | t.currentDbId( parseInt(argv[1]) ); |
| | } |
| | } |
| |
|
| | |
| | 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); |
| | |
| | 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); |
| | } |
| | } |
| | } |
| |
|
| | |
| | class NotGlobCommand extends GlobCommand { |
| | constructor(){super(true);} |
| | } |
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | |
| | class NewDbCommand extends OpenDbCommand { |
| | constructor(){ super(true); } |
| | } |
| |
|
| | |
| | class NoopCommand extends Command { |
| | process(t, ts, argv){} |
| | } |
| |
|
| | |
| | class NullCommand extends Command { |
| | process(st, ts, argv){ |
| | this.argcCheck(ts,argv,1); |
| | st.nullValue( argv[1] ); |
| | } |
| | } |
| |
|
| | |
| | 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) ); |
| | } |
| | } |
| | } |
| |
|
| | |
| | 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(); |
| | |
| | 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."); |
| | } |
| | } |
| | } |
| |
|
| | |
| | class JsonCommand extends ResultCommand { |
| | constructor(){ super(ResultBufferMode.ASIS); } |
| | } |
| |
|
| | |
| | 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); |
| | } |
| | } |
| | } |
| |
|
| | |
| | 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(); |
| | |
| | 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],">>"); |
| | } |
| | } |
| | } |
| | } |
| |
|
| | |
| | class JsonBlockCommand extends TableResultCommand { |
| | constructor(){ super(true); } |
| | } |
| |
|
| | |
| | class TestCaseCommand extends Command { |
| | process(tester, script, argv){ |
| | this.argcCheck(script, argv,1); |
| | script.testCaseName(argv[1]); |
| | tester.clearResultBuffer(); |
| | tester.clearInputBuffer(); |
| | } |
| | } |
| |
|
| |
|
| | |
| | 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); |
| | } |
| | } |
| |
|
| | const namespace = newObj({ |
| | Command, |
| | DbException, |
| | IncompatibleDirective, |
| | Outer, |
| | SQLTester, |
| | SQLTesterException, |
| | TestScript, |
| | TestScriptFailed, |
| | UnknownCommand, |
| | Util, |
| | sqlite3 |
| | }); |
| |
|
| | export {namespace as default}; |
| |
|