diff options
Diffstat (limited to 'ext/wasm')
-rw-r--r-- | ext/wasm/SQLTester/SQLTester.mjs | 352 | ||||
-rw-r--r-- | ext/wasm/SQLTester/SQLTester.run.mjs | 53 | ||||
-rw-r--r-- | ext/wasm/api/sqlite3-api-prologue.js | 8 | ||||
-rw-r--r-- | ext/wasm/api/sqlite3-wasm.c | 112 |
4 files changed, 473 insertions, 52 deletions
diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs index 0461beb86..c7059ad1b 100644 --- a/ext/wasm/SQLTester/SQLTester.mjs +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -73,7 +73,7 @@ class DbException extends SQLTesterException { class TestScriptFailed extends SQLTesterException { constructor(testScript, ...args){ - super(testScript.getPutputPrefix(),': ',...args); + super(testScript.getOutputPrefix(),': ',...args); } isFatal() { return true; } } @@ -103,6 +103,18 @@ 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" characters - we have to escape output if it contains any. + special: /[\x00-\x20\x22\x5c\x7b\x7d]/, + //! Either of '{' or '}'. + squiggly: /[{}]/ +}); + const Util = newObj({ toss, @@ -110,7 +122,11 @@ const Util = newObj({ return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn); }, - argvToString: (list)=>list.join(" "), + argvToString: (list)=>{ + const m = [...list]; + m.shift(); + return m.join(" ") + }, utf8Decode: function(arrayBuffer, begin, end){ return __utf8Decoder.decode( @@ -120,7 +136,10 @@ const Util = newObj({ ); }, - utf8Encode: (str)=>__utf8Encoder.encode(str) + utf8Encode: (str)=>__utf8Encoder.encode(str), + + strglob: sqlite3.wasm.xWrap('sqlite3_wasm_SQLTester_strglob','int', + ['string','string']) })/*Util*/; class Outer { @@ -182,21 +201,39 @@ class Outer { class SQLTester { + //! Console output utility. #outer = new Outer().outputPrefix( ()=>'SQLTester: ' ); + //! List of input script files. #aFiles = []; + //! Test input buffer. #inputBuffer = []; + //! Test result buffer. #resultBuffer = []; + //! Output representation of SQL NULL. #nullView = "nil"; - #metrics = newObj({ - nTotalTest: 0, nTestFile: 0, nAbortedScript: 0 + metrics = newObj({ + //! Total tests run + nTotalTest: 0, + //! Total test script files run + nTestFile: 0, + //! Number of scripts which were aborted + nAbortedScript: 0, + //! Incremented by test case handlers + nTest: 0 }); #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]; } @@ -208,12 +245,17 @@ class SQLTester { outln(...args){ return this.#outer.outln(...args); } out(...args){ return this.#outer.out(...args); } + incrementTestCounter(){ + ++this.metrics.nTotalTest; + ++this.metrics.nTest; + } + reset(){ this.clearInputBuffer(); this.clearResultBuffer(); this.#clearBuffer(this.#db.initSql); this.closeAllDbs(); - this.nTest = 0; + this.metrics.nTest = 0; this.nullView = "nil"; this.emitColNames = false; this.#db.iCurrentDb = 0; @@ -365,7 +407,7 @@ class SQLTester { Util.unlink(this.#db.initialDbName); this.openDb(0, this.#db.initialDbName, true); }else{ - this.#outer.outln("WARNING: setupInitialDb() unexpectedly ", + this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ", "triggered while it is opened."); } } @@ -405,17 +447,107 @@ class SQLTester { #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) ){ + if( '{' === msg.charAt(0) ){ sb.push(msg); }else{ sb.push('{', msg, '}'); } } + #checkDbRc(pDb,rc){ + sqlite3.oo1.DB.checkRc(pDb, rc); + } + execSql(pDb, throwOnError, appendMode, lineMode, sql){ - sql = sqlite3.capi.sqlite3_js_sql_to_string(sql); - this.#outer.outln("execSql() is TODO. ",sql); - return 0; + 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 + : new TextEncoder("utf-8").encode(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(pDb, rc); + }else if( sb ){ + self.#appendDbErr(db, 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 === lineMode ){ + 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*/ @@ -469,17 +601,6 @@ class Cursor { } } -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]/, - //! Either of '{' or '}'. - squiggly: /[{}]/ -}); - class TestScript { #cursor = new Cursor(); #moduleName = null; @@ -529,6 +650,28 @@ class TestScript { 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()); @@ -621,14 +764,16 @@ class TestScript { const oldPB = cur.putbackPos; const oldPBL = cur.putbackLineNo; const oldLine = cur.lineNo; - const rc = this.getLine(); - cur.peekedPos = cur.pos; - cur.peekedLineNo = cur.lineNo; - cur.pos = oldPos; - cur.lineNo = oldLine; - cur.putbackPos = oldPB; - cur.putbackLineNo = oldPBL; - return rc; + 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; + } } @@ -667,7 +812,7 @@ class CloseDbCommand extends Command { let id; if(argv.length>1){ const arg = argv[1]; - if("all".equals(arg)){ + if( "all" === arg ){ t.closeAllDbs(); return; } @@ -697,6 +842,36 @@ class DbCommand extends Command { } } +//! --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; @@ -740,6 +915,107 @@ class PrintCommand extends Command { } } +//! --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.verbose1(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); + log("TRC fetchCommandBody: ",body); + 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); + log("TRC fetchCommandBody reshaped:",body); + } + const globs = body.split(/\s*\n\s*/); + if( globs.length < 1 ){ + ts.toss(argv[0], " requires 1 or more ", + (this.#jsonMode ? "json snippets" : "globs"),"."); + } + log("TRC fetchCommandBody globs:",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/); + log("TRC fetchCommandBody rbuf, res:",rbuf, res); + 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 { @@ -770,18 +1046,18 @@ class CommandDispatcher { 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 "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 "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 "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; } diff --git a/ext/wasm/SQLTester/SQLTester.run.mjs b/ext/wasm/SQLTester/SQLTester.run.mjs index dc8eaa0c1..36d1ab5dc 100644 --- a/ext/wasm/SQLTester/SQLTester.run.mjs +++ b/ext/wasm/SQLTester/SQLTester.run.mjs @@ -25,14 +25,46 @@ log("ns =",ns); out("Hi there. ").outln("SQLTester is ostensibly ready."); let ts = new ns.TestScript('/foo.test', ns.Util.utf8Encode( -`# comment line ---print Starting up... ---null NIL ---new :memory: ---testcase 0.0.1 -select '0.0.1'; -#--result 0.0.1 ---print done +` +--close all +--oom +--db 0 +--new my.db +--null zilch +--testcase 1.0 +SELECT 1, null; +--result 1 zilch +--glob *zil* +--notglob *ZIL* +SELECT 1, 2; +intentional error; +--run +--testcase json-1 +SELECT json_array(1,2,3) +--json [1,2,3] +--testcase tableresult-1 + select 1, 'a'; + select 2, 'b'; +--tableresult + # [a-z] + 2 b +--end +--testcase json-block-1 + select json_array(1,2,3); + select json_object('a',1,'b',2); +--json-block + [1,2,3] + {"a":1,"b":2} +--end +--testcase col-names-on +--column-names 1 + select 1 as 'a', 2 as 'b'; +--result a 1 b 2 +--testcase col-names-off +--column-names 0 + select 1 as 'a', 2 as 'b'; +--result 1 2 +--close `)); const sqt = new ns.SQLTester(); @@ -41,11 +73,12 @@ try{ sqt.openDb('/foo.db', true); log( 'sqt.getCurrentDb()', sqt.getCurrentDb() ); sqt.verbosity(0); - affirm( 'NIL' !== sqt.nullValue() ); + affirm( 'zilch' !== sqt.nullValue() ); ts.run(sqt); - affirm( 'NIL' === sqt.nullValue() ); + affirm( 'zilch' === sqt.nullValue() ); }finally{ sqt.reset(); } log( 'sqt.getCurrentDb()', sqt.getCurrentDb() ); +log( "Metrics:", sqt.metrics ); diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index 35c856269..996f23298 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -1139,15 +1139,15 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( /** Records the current pstack position, calls the given function, - and restores the pstack regardless of whether the function - throws. Returns the result of the call or propagates an - exception on error. + passing it the sqlite3 object, then restores the pstack + regardless of whether the function throws. Returns the result + of the call or propagates an exception on error. Added in 3.44. */ call: function(f){ const stackPos = wasm.pstack.pointer; - try{ return f() }finally{ + try{ return f(sqlite3) } finally{ wasm.pstack.restore(stackPos); } } diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c index ff15e3b4f..db77010d9 100644 --- a/ext/wasm/api/sqlite3-wasm.c +++ b/ext/wasm/api/sqlite3-wasm.c @@ -1801,6 +1801,118 @@ char * sqlite3_wasm_test_str_hello(int fail){ } return s; } + +/* +** For testing using SQLTester scripts. +** +** Return non-zero if string z matches glob pattern zGlob and zero if the +** pattern does not match. +** +** To repeat: +** +** zero == no match +** non-zero == match +** +** Globbing rules: +** +** '*' Matches any sequence of zero or more characters. +** +** '?' Matches exactly one character. +** +** [...] Matches one character from the enclosed list of +** characters. +** +** [^...] Matches one character not in the enclosed list. +** +** '#' Matches any sequence of one or more digits with an +** optional + or - sign in front, or a hexadecimal +** literal of the form 0x... +*/ +static int sqlite3_wasm_SQLTester_strnotglob(const char *zGlob, const char *z){ + int c, c2; + int invert; + int seen; + typedef int (*recurse_f)(const char *,const char *); + static const recurse_f recurse = sqlite3_wasm_SQLTester_strnotglob; + + while( (c = (*(zGlob++)))!=0 ){ + if( c=='*' ){ + while( (c=(*(zGlob++))) == '*' || c=='?' ){ + if( c=='?' && (*(z++))==0 ) return 0; + } + if( c==0 ){ + return 1; + }else if( c=='[' ){ + while( *z && recurse(zGlob-1,z)==0 ){ + z++; + } + return (*z)!=0; + } + while( (c2 = (*(z++)))!=0 ){ + while( c2!=c ){ + c2 = *(z++); + if( c2==0 ) return 0; + } + if( recurse(zGlob,z) ) return 1; + } + return 0; + }else if( c=='?' ){ + if( (*(z++))==0 ) return 0; + }else if( c=='[' ){ + int prior_c = 0; + seen = 0; + invert = 0; + c = *(z++); + if( c==0 ) return 0; + c2 = *(zGlob++); + if( c2=='^' ){ + invert = 1; + c2 = *(zGlob++); + } + if( c2==']' ){ + if( c==']' ) seen = 1; + c2 = *(zGlob++); + } + while( c2 && c2!=']' ){ + if( c2=='-' && zGlob[0]!=']' && zGlob[0]!=0 && prior_c>0 ){ + c2 = *(zGlob++); + if( c>=prior_c && c<=c2 ) seen = 1; + prior_c = 0; + }else{ + if( c==c2 ){ + seen = 1; + } + prior_c = c2; + } + c2 = *(zGlob++); + } + if( c2==0 || (seen ^ invert)==0 ) return 0; + }else if( c=='#' ){ + if( z[0]=='0' + && (z[1]=='x' || z[1]=='X') + && sqlite3Isxdigit(z[2]) + ){ + z += 3; + while( sqlite3Isxdigit(z[0]) ){ z++; } + }else{ + if( (z[0]=='-' || z[0]=='+') && sqlite3Isdigit(z[1]) ) z++; + if( !sqlite3Isdigit(z[0]) ) return 0; + z++; + while( sqlite3Isdigit(z[0]) ){ z++; } + } + }else{ + if( c!=(*(z++)) ) return 0; + } + } + return *z==0; +} + +SQLITE_WASM_EXPORT +int sqlite3_wasm_SQLTester_strglob(const char *zGlob, const char *z){ + return !sqlite3_wasm_SQLTester_strnotglob(zGlob, z); +} + + #endif /* SQLITE_WASM_TESTS */ #undef SQLITE_WASM_EXPORT |