From 524ddc940d47184bd83771781a6dbd472ccc6365 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 29 Aug 2023 11:22:45 +0000 Subject: Init bits of a port of Java's SQLTester to JS. Far from complete. FossilOrigin-Name: 60eec5ceda80c64870713df8e9aeabeef933c007f2010792225a07d5ef36baef --- ext/wasm/SQLTester/SQLTester.mjs | 290 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 ext/wasm/SQLTester/SQLTester.mjs (limited to 'ext/wasm/SQLTester/SQLTester.mjs') diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs new file mode 100644 index 000000000..c295bbd84 --- /dev/null +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -0,0 +1,290 @@ +/* +** 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. +*/ + +// UNDER CONSTRUCTION. Still being ported from the Java impl. + +import sqlite3ApiInit from '/jswasm/sqlite3.mjs'; + +const sqlite3 = await sqlite3ApiInit(); + +const log = (...args)=>{ + console.log('SQLTester:',...args); +}; + +// 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(...args){ + super(args.join(' ')); + } + isFatal() { return false; } +} + +SQLTesterException.toss = (...args)=>{ + throw new SQLTesterException(...args); +} + +class DbException extends SQLTesterException { + constructor(...args){ + super(...args); + //TODO... + //const db = args[0]; + //if( db instanceof sqlite3.oo1.DB ) + } + isFatal() { return true; } +} + +class TestScriptFailed extends SQLTesterException { + constructor(...args){ + super(...args); + } + isFatal() { return true; } +} + +class UnknownCommand extends SQLTesterException { + constructor(...args){ + super(...args); + } +} + +class IncompatibleDirective extends SQLTesterException { + constructor(...args){ + super(...args); + } +} + +const toss = (errType, ...args)=>{ + if( !(errType instanceof SQLTesterException)){ + args.unshift(errType); + errType = SQLTesterException; + } + throw new errType(...args); +}; + +const __utf8Decoder = new TextDecoder(); +const __utf8Encoder = new TextEncoder('utf-8'); +const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer) + ? function(){} : globalThis.SharedArrayBuffer; + +const Util = newObj({ + toss, + + unlink: function(fn){ + return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn); + }, + + argvToString: (list)=>list.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) +})/*Util*/; + +class Outer { + #lnBuf = []; + #verbosity = 0; + #logger = console.log.bind(console); + + out(...args){ + this.#lnBuf.append(...args); + return this; + } + outln(...args){ + this.#lnBuf.append(...args,'\n'); + this.logger(this.#lnBuf.join('')); + this.#lnBuf.length = 0; + return this; + } + + #verboseN(lvl, argv){ + if( this.#verbosity>=lvl ){ + const pre = this.getOutputPrefix ? this.getOutputPrefix() : ''; + this.outln('VERBOSE ',lvl,' ',pre,': ',...argv); + } + } + verbose1(...args){ return this.#verboseN(1,args); } + verbose2(...args){ return this.#verboseN(2,args); } + verbose3(...args){ return this.#verboseN(3,args); } + + verbosity(){ + let rc; + if(arguments.length){ + rc = this.#verbosity; + this.#verbosity = arguments[0]; + }else{ + rc = this.#verbosity; + } + return rc; + } + +}/*Outer*/ + +class SQLTester { + SQLTester(){} + + #aFiles = []; + #inputBuffer = []; + #outputBuffer = []; + #resultBuffer = []; + #nullView = "nil"; + #metrics = newObj({ + nTotalTest: 0, nTestFile: 0, nAbortedScript: 0 + }); + #emitColNames = false; + #keepGoing = false; + #aDb = []; + #db = newObj({ + list: [], + iCurrent: 0, + initialDbName: "test.db", + }); + +}/*SQLTester*/ + +class Command { + Command(){ + } + 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; + buffer = []; + 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; + + //! Restore parsing state to the start of the stream. + rewind(){ + this.buffer.length = 0; + this.pos = this.lineNo = this.putbackPos = + this.putbackLineNo = this.peekedPos = this.peekedLineNo = 0; + } +} + +class TestScript { + #cursor = new Cursor(); + #verbosity = 0; + #moduleName = null; + #filename = null; + #testCaseName = null; + #outer = new Outer(); + #verboseN(lvl, argv){ + if( this.#verbosity>=lvl ){ + this.outln('VERBOSE ',lvl,': ',...argv); + } + } + + verbose1(...args){ return this.#verboseN(1,args); } + verbose2(...args){ return this.#verboseN(2,args); } + verbose3(...args){ return this.#verboseN(3,args); } + + TestScript(content){ + this.cursor.src = content; + this.outer.outputPrefix = ()=>this.getOutputPrefix(); + } + + verbosity(){ + let rc; + if(arguments.length){ + rc = this.#verbosity; + this.#verbosity = arguments[0]; + }else{ + rc = this.#verbosity; + } + return rc; + } + + getOutputPrefix() { + const rc = "["+(this.moduleName || this.filename)+"]"; + if( this.testCaseName ) rc += "["+this.testCaseName+"]"; + return rc + " line "+ this.cur.lineNo; + } + + toss(...args){ + Util.toss(this.getOutputPrefix()+":",TestScriptFailed,...args) + } + +}/*TestScript*/; + + +const namespace = newObj({ + SQLTester: new SQLTester(), + DbException, + IncompatibleDirective, + SQLTesterException, + TestScriptFailed, + UnknownCommand +}); + + +export {namespace as default}; -- cgit v1.2.3 From 0fc20a32c0fce51c34714363173c8a93771c1425 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 29 Aug 2023 13:28:36 +0000 Subject: Get the basic parsing pieces and command dispatching in place in the JS SQLTester. FossilOrigin-Name: 8fcc2a553c1e26734902bbdee0c38183ee22b7b5c75f07405529bb79db34145a --- ext/wasm/SQLTester/SQLTester.mjs | 314 +++++++++++++++++++++++++++++++++------ 1 file changed, 268 insertions(+), 46 deletions(-) (limited to 'ext/wasm/SQLTester/SQLTester.mjs') diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs index c295bbd84..3af83e9e7 100644 --- a/ext/wasm/SQLTester/SQLTester.mjs +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -54,7 +54,7 @@ const ResultRowMode = newObj({ class SQLTesterException extends globalThis.Error { constructor(...args){ - super(args.join(' ')); + super(args.join('')); } isFatal() { return false; } } @@ -74,8 +74,8 @@ class DbException extends SQLTesterException { } class TestScriptFailed extends SQLTesterException { - constructor(...args){ - super(...args); + constructor(testScript, ...args){ + super(testScript.getPutputPrefix(),': ',...args); } isFatal() { return true; } } @@ -130,26 +130,39 @@ class Outer { #verbosity = 0; #logger = console.log.bind(console); + constructor(){ + } + out(...args){ - this.#lnBuf.append(...args); + if(!this.#lnBuf.length && this.getOutputPrefix ){ + this.#lnBuf.push(this.getOutputPrefix()); + } + this.#lnBuf.push(...args); return this; } outln(...args){ - this.#lnBuf.append(...args,'\n'); - this.logger(this.#lnBuf.join('')); + if(!this.#lnBuf.length && this.getOutputPrefix ){ + this.#lnBuf.push(this.getOutputPrefix()); + } + this.#lnBuf.push(...args,'\n'); + this.#logger(this.#lnBuf.join('')); this.#lnBuf.length = 0; return this; } - #verboseN(lvl, argv){ + setOutputPrefix( func ){ + this.getOutputPrefix = func; + return this; + } + + verboseN(lvl, argv){ if( this.#verbosity>=lvl ){ - const pre = this.getOutputPrefix ? this.getOutputPrefix() : ''; - this.outln('VERBOSE ',lvl,' ',pre,': ',...argv); + this.outln('VERBOSE ',lvl,': ',...argv); } } - verbose1(...args){ return this.#verboseN(1,args); } - verbose2(...args){ return this.#verboseN(2,args); } - verbose3(...args){ return this.#verboseN(3,args); } + verbose1(...args){ return this.verboseN(1,args); } + verbose2(...args){ return this.verboseN(2,args); } + verbose3(...args){ return this.verboseN(3,args); } verbosity(){ let rc; @@ -165,11 +178,10 @@ class Outer { }/*Outer*/ class SQLTester { - SQLTester(){} + #outer = new Outer().setOutputPrefix( ()=>'SQLTester: ' ); #aFiles = []; #inputBuffer = []; - #outputBuffer = []; #resultBuffer = []; #nullView = "nil"; #metrics = newObj({ @@ -184,14 +196,42 @@ class SQLTester { initialDbName: "test.db", }); + constructor(){ + } + + 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'); + } + + clearInputBuffer(){ + this.#inputBuffer.length = 0; + return this.#inputBuffer; + } + clearResultBuffer(){ + this.#resultBuffer.length = 0; + return this.#resultBuffer; + } + + getInputText(){ return this.#inputBuffer.join(''); } + getResultText(){ return this.#resultBuffer.join(''); } + + verbosity(...args){ return this.#outer.verbosity(...args); } + }/*SQLTester*/ class Command { - 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)){ @@ -203,13 +243,22 @@ class Command { testScript.toss(argv[0]," requires at least ",min," arguments."); } } + } +} +class TestCase extends Command { + + process(tester, script, argv){ + this.argcCheck(script, argv,1); + script.testCaseName(argv[1]); + tester.clearResultBuffer(); + tester.clearInputBuffer(); } } class Cursor { src; - buffer = []; + sb = []; pos = 0; //! Current line number. Starts at 0 for internal reasons and will // line up with 1-based reality once parsing starts. @@ -223,68 +272,241 @@ class Cursor { //! Peeked-to line number. peekedLineNo = 0; + constructor(){ + } + //! Restore parsing state to the start of the stream. rewind(){ - this.buffer.length = 0; - this.pos = this.lineNo = this.putbackPos = - this.putbackLineNo = this.peekedPos = this.peekedLineNo = 0; + this.sb.length = this.pos = this.lineNo + = this.putbackPos = this.putbackLineNo + = this.peekedPos = this.peekedLineNo = 0; } } +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-]+)( .*)?)$/ +}); + class TestScript { #cursor = new Cursor(); - #verbosity = 0; #moduleName = null; #filename = null; #testCaseName = null; - #outer = new Outer(); - #verboseN(lvl, argv){ - if( this.#verbosity>=lvl ){ - this.outln('VERBOSE ',lvl,': ',...argv); + #outer = new Outer().setOutputPrefix( ()=>this.getOutputPrefix() ); + + constructor(...args){ + let content, filename; + if( 2 == args.length ){ + filename = args[0]; + content = args[1]; + }else{ + content = args[0]; } + this.#filename = filename; + this.#cursor.src = content; + this.#outer.outputPrefix = ()=>this.getOutputPrefix(); } - verbose1(...args){ return this.#verboseN(1,args); } - verbose2(...args){ return this.#verboseN(2,args); } - verbose3(...args){ return this.#verboseN(3,args); } + testCaseName(){ + return (0==arguments.length) + ? this.#testCaseName : (this.#testCaseName = arguments[0]); + } - TestScript(content){ - this.cursor.src = content; - this.outer.outputPrefix = ()=>this.getOutputPrefix(); + getOutputPrefix() { + let rc = "["+(this.#moduleName || this.#filename)+"]"; + if( this.#testCaseName ) rc += "["+this.#testCaseName+"]"; + return rc + " line "+ this.#cursor.lineNo +" "; } - verbosity(){ - let rc; - if(arguments.length){ - rc = this.#verbosity; - this.#verbosity = arguments[0]; - }else{ - rc = this.#verbosity; + reset(){ + this.#testCaseName = null; + this.#cursor.rewind(); + return this; + } + + toss(...args){ + throw new TestScriptFailed(this,...args); + } + + #checkForDirective(tester,line){ + //todo + } + + #getCommandArgv(line){ + const m = Rx.command.exec(line); + return m ? m[1].trim().split(/\s+/) : null; + } + + run(tester){ + this.reset(); + this.#outer.verbosity(tester.verbosity()); + let line, directive, argv = []; + while( null != (line = this.getLine()) ){ + this.verbose3("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.verbose1("running command: ",argv[0], " ", Util.argvToString(argv)); + if(this.#outer.verbosity()>1){ + const input = tester.getInputText(); + if( !!input ) this.verbose3("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; + 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; } - getOutputPrefix() { - const rc = "["+(this.moduleName || this.filename)+"]"; - if( this.testCaseName ) rc += "["+this.testCaseName+"]"; - return rc + " line "+ this.cur.lineNo; + + /** + 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; } - toss(...args){ - Util.toss(this.getOutputPrefix()+":",TestScriptFailed,...args) + /** + 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; } + 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){ return this.#outer.verbosity(...args); } + }/*TestScript*/; +class CommandDispatcher { + static map = newObj(); + + static getCommandByName(name){ + let rv = CommandDispatcher.map[name]; + if( rv ) return rv; + switch(name){ + //todo: map name to Command instance + case "testcase": rv = new TestCase(); break; + } + if( rv ){ + CommandDispatcher.map[name] = rv; + } + return rv; + } + + static dispatch(tester, testScript, argv){ + const cmd = CommandDispatcher.getCommandByName(argv[0]); + if( !cmd ){ + toss(UnknownCommand,argv[0],' ',testScript.getOutputPrefix()); + } + cmd.process(tester, testScript, argv); + } +}/*CommandDispatcher*/ const namespace = newObj({ - SQLTester: new SQLTester(), + Command, DbException, IncompatibleDirective, + Outer, + SQLTester, SQLTesterException, + TestScript, TestScriptFailed, - UnknownCommand + UnknownCommand, + Util }); - export {namespace as default}; -- cgit v1.2.3 From 69a55ca17dc711a9b75eb738ab32336936d69fd7 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 29 Aug 2023 15:39:57 +0000 Subject: Get the JS SQLTester command handlers in place sans those which have to run SQL. FossilOrigin-Name: d21b1217964a53f33b7ba3958b34aa8560dff8ede33e66f54aa0afbab7099ec3 --- ext/wasm/SQLTester/SQLTester.mjs | 377 +++++++++++++++++++++++++++++++++++---- 1 file changed, 341 insertions(+), 36 deletions(-) (limited to 'ext/wasm/SQLTester/SQLTester.mjs') diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs index 3af83e9e7..0461beb86 100644 --- a/ext/wasm/SQLTester/SQLTester.mjs +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -64,11 +64,9 @@ SQLTesterException.toss = (...args)=>{ } class DbException extends SQLTesterException { - constructor(...args){ - super(...args); - //TODO... - //const db = args[0]; - //if( db instanceof sqlite3.oo1.DB ) + constructor(pDb, rc, closeDb){ + super("DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb)); + if( closeDb ) sqlite3.capi.sqlite3_close_v2(pDb); } isFatal() { return true; } } @@ -150,9 +148,14 @@ class Outer { return this; } - setOutputPrefix( func ){ - this.getOutputPrefix = func; - return this; + outputPrefix(){ + if( 0==arguments.length ){ + return (this.getOutputPrefix + ? (this.getOutputPrefix() ?? '') : ''); + }else{ + this.getOutputPrefix = arguments[0]; + return this; + } } verboseN(lvl, argv){ @@ -179,7 +182,7 @@ class Outer { class SQLTester { - #outer = new Outer().setOutputPrefix( ()=>'SQLTester: ' ); + #outer = new Outer().outputPrefix( ()=>'SQLTester: ' ); #aFiles = []; #inputBuffer = []; #resultBuffer = []; @@ -189,16 +192,34 @@ class SQLTester { }); #emitColNames = false; #keepGoing = false; - #aDb = []; #db = newObj({ - list: [], - iCurrent: 0, + list: new Array(7), + iCurrentDb: 0, initialDbName: "test.db", + initSql: ['select 1;'], + currentDb: function(){ + return this.list[this.iCurrentDb]; + } }); constructor(){ } + outln(...args){ return this.#outer.outln(...args); } + out(...args){ return this.#outer.out(...args); } + + reset(){ + this.clearInputBuffer(); + this.clearResultBuffer(); + this.#clearBuffer(this.#db.initSql); + this.closeAllDbs(); + this.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'); @@ -208,20 +229,195 @@ class SQLTester { if( addNL ) this.#resultBuffer.push('\n'); } - clearInputBuffer(){ - this.#inputBuffer.length = 0; - return this.#inputBuffer; - } - clearResultBuffer(){ - this.#resultBuffer.length = 0; - return this.#resultBuffer; + #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); + } + verbosity(...args){ return this.#outer.verbosity(...args); } + nullValue(){ + if( 0==arguments.length ){ + return this.#nullView; + }else{ + this.#nullView = ''+arguments[0]; + } + } + + outputColumnNames(){ + if( 0==arguments.length ){ + return this.#emitColNames; + }else{ + this.#emitColNames = !!arguments[0]; + } + } + + currentDbId(){ + if( 0==arguments.length ){ + return this.#db.iCurrentDb; + }else{ + 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 db = this.#affirmDbId(id).#db.list[id]; + if( db ){ + sqlite3.capi.sqlite3_close_v2(db); + this.#db.list[id] = null; + } + } + + closeAllDbs(){ + for(let i = 0; i 0){ + //this.#outer.verbose2("RUNNING DB INIT CODE: ",this.#db.initSql.toString()); + rc = this.execSql(pDb, false, ResultBufferMode.NONE, + null, this.#db.initSql.join('')); + } + 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; + } + } + + #setupInitialDb(){ + if( !this.#db.list[0] ){ + Util.unlink(this.#db.initialDbName); + this.openDb(0, this.#db.initialDbName, true); + }else{ + this.#outer.outln("WARNING: setupInitialDb() unexpectedly ", + "triggered while it is opened."); + } + } + + /** + Returns v or some escaped form of v, as defined in the tester's + spec doc. + */ + #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, '}'); + } + } + + execSql(pDb, throwOnError, appendMode, lineMode, sql){ + sql = sqlite3.capi.sqlite3_js_sql_to_string(sql); + this.#outer.outln("execSql() is TODO. ",sql); + return 0; + } + }/*SQLTester*/ class Command { @@ -246,16 +442,6 @@ class Command { } } -class TestCase extends Command { - - process(tester, script, argv){ - this.argcCheck(script, argv,1); - script.testCaseName(argv[1]); - tester.clearResultBuffer(); - tester.clearInputBuffer(); - } -} - class Cursor { src; sb = []; @@ -287,7 +473,11 @@ 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-]+)( .*)?)$/ + 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 { @@ -295,7 +485,7 @@ class TestScript { #moduleName = null; #filename = null; #testCaseName = null; - #outer = new Outer().setOutputPrefix( ()=>this.getOutputPrefix() ); + #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' ); constructor(...args){ let content, filename; @@ -307,7 +497,6 @@ class TestScript { } this.#filename = filename; this.#cursor.src = content; - this.#outer.outputPrefix = ()=>this.getOutputPrefix(); } testCaseName(){ @@ -318,7 +507,7 @@ class TestScript { getOutputPrefix() { let rc = "["+(this.#moduleName || this.#filename)+"]"; if( this.#testCaseName ) rc += "["+this.#testCaseName+"]"; - return rc + " line "+ this.#cursor.lineNo +" "; + return rc + " line "+ this.#cursor.lineNo; } reset(){ @@ -471,6 +660,106 @@ class TestScript { }/*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".equals(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]) ); + } +} + +//! --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) ); + } + } +} + + +//! --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){ + t.argcCheck(ts,argv,1); + ts.verbosity( parseInt(argv[1]) ); + } +} + class CommandDispatcher { static map = newObj(); @@ -478,8 +767,23 @@ class CommandDispatcher { let rv = CommandDispatcher.map[name]; if( rv ) return rv; switch(name){ - //todo: map name to Command instance - case "testcase": rv = new TestCase(); break; + 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; @@ -506,7 +810,8 @@ const namespace = newObj({ TestScript, TestScriptFailed, UnknownCommand, - Util + Util, + sqlite3 }); export {namespace as default}; -- cgit v1.2.3 From aa150477961c57bc0c873faf95f0bc600fc73af6 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 29 Aug 2023 20:01:01 +0000 Subject: JS SQLTestRunner can now run the Java impl's core-most sanity tests, missing only support for directives. FossilOrigin-Name: 5e798369375ce1b0c9cdf831f835d931fbd562ff7b4db09a06d1bdca2ac1b975 --- ext/wasm/SQLTester/SQLTester.mjs | 352 ++++++++++++++++++++++++++++++++++----- 1 file changed, 314 insertions(+), 38 deletions(-) (limited to 'ext/wasm/SQLTester/SQLTester.mjs') 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; } -- cgit v1.2.3 From 267c44771fec6758c371eb41d0ab99a9ea0c8452 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 29 Aug 2023 20:44:40 +0000 Subject: More fleshing out of JS SQLTester. FossilOrigin-Name: 8c503dfb9fa15389613a819fcc1792e23d3c05f99a9f450f82eac5125298726f --- ext/wasm/SQLTester/SQLTester.mjs | 69 +++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 8 deletions(-) (limited to 'ext/wasm/SQLTester/SQLTester.mjs') diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs index c7059ad1b..c8ab2fa5c 100644 --- a/ext/wasm/SQLTester/SQLTester.mjs +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -203,8 +203,8 @@ class SQLTester { //! Console output utility. #outer = new Outer().outputPrefix( ()=>'SQLTester: ' ); - //! List of input script files. - #aFiles = []; + //! List of input scripts. + #aScripts = []; //! Test input buffer. #inputBuffer = []; //! Test result buffer. @@ -259,7 +259,7 @@ class SQLTester { this.nullView = "nil"; this.emitColNames = false; this.#db.iCurrentDb = 0; - this.#db.initSql.push("SELECT 1;"); + //this.#db.initSql.push("SELECT 1;"); } appendInput(line, addNL){ @@ -402,6 +402,54 @@ class SQLTester { } } + 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(); + for(const ts of this.#aScripts){ + this.reset(); + ++this.metrics.nTestFile; + let threw = false; + const timeStart = (new Date()).getTime(); + try{ + ts.run(this); + }catch(e){ + if(e instanceof SQLTesterException){ + threw = true; + this.outln("🔥EXCEPTION: ",''+e); + ++this.metrics.nAbortedScript; + if( this.#keepGoing ){ + this.outln("Continuing anyway becaure of the keep-going option."); + } + else if( e.isFatal() ) throw e; + }else{ + throw e; + } + }finally{ + const timeEnd = (new Date()).getTime(); + this.outln("🏁", (threw ? "❌" : "✅"), " ", this.metrics.nTest, + " test(s) in ", (timeEnd-timeStart),"ms."); + } + } + const tEnd = (new Date()).getTime(); + this.outln("Total run-time: ",(tEnd-tStart),"ms"); + Util.unlink(this.#db.initialDbName); + return this; + } + #setupInitialDb(){ if( !this.#db.list[0] ){ Util.unlink(this.#db.initialDbName); @@ -466,7 +514,7 @@ class SQLTester { const wasm = sqlite3.wasm, capi = sqlite3.capi; sql = (sql instanceof Uint8Array) ? sql - : new TextEncoder("utf-8").encode(capi.sqlite3_js_sql_to_string(sql)); + : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql)); const self = this; const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer; let rc = 0; @@ -616,6 +664,15 @@ class TestScript { }else{ content = args[0]; } + if(!(content instanceof Uint8Array)){ + if('string' === typeof content){ + content = Util.utf8Encode(content); + }else if(content instanceof ArrayBuffer){ + content = new Uint8Array(content); + }else{ + toss(Error, "Invalid content type for TestScript constructor."); + } + } this.#filename = filename; this.#cursor.src = content; } @@ -971,28 +1028,24 @@ class TableResultCommand extends Command { 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); -- cgit v1.2.3 From 4f1387e9ab1b5aed8c1c901be23eb02af47854d6 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 29 Aug 2023 21:30:37 +0000 Subject: Add directives support to JS SQLTester comparable to the Java impl. This brings the two to feature parity. FossilOrigin-Name: 7cef4a8300826adbdcb3b205e134a4272b12b4aa7dbee97731ac12282a4a9f06 --- ext/wasm/SQLTester/SQLTester.mjs | 72 ++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 21 deletions(-) (limited to 'ext/wasm/SQLTester/SQLTester.mjs') diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs index c8ab2fa5c..aa02d7a5e 100644 --- a/ext/wasm/SQLTester/SQLTester.mjs +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -53,8 +53,13 @@ const ResultRowMode = newObj({ }); class SQLTesterException extends globalThis.Error { - constructor(...args){ - super(args.join('')); + constructor(testScript, ...args){ + if(testScript){ + super( [testScript.getOutputPrefix()+": ", ...args].join('') ); + }else{ + super( args.join('') ); + } + this.name = 'SQLTesterException'; } isFatal() { return false; } } @@ -64,8 +69,9 @@ SQLTesterException.toss = (...args)=>{ } class DbException extends SQLTesterException { - constructor(pDb, rc, closeDb){ - super("DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb)); + constructor(testScript, pDb, rc, closeDb){ + super(testScript, "DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb)); + this.name = 'DbException'; if( closeDb ) sqlite3.capi.sqlite3_close_v2(pDb); } isFatal() { return true; } @@ -73,28 +79,27 @@ class DbException extends SQLTesterException { class TestScriptFailed extends SQLTesterException { constructor(testScript, ...args){ - super(testScript.getOutputPrefix(),': ',...args); + super(testScript,...args); + this.name = 'TestScriptFailed'; } isFatal() { return true; } } class UnknownCommand extends SQLTesterException { - constructor(...args){ - super(...args); + constructor(testScript, cmdName){ + super(testScript, cmdName); + this.name = 'UnknownCommand'; } } class IncompatibleDirective extends SQLTesterException { - constructor(...args){ - super(...args); + constructor(testScript, ...args){ + super(testScript,...args); + this.name = 'IncompatibleDirective'; } } const toss = (errType, ...args)=>{ - if( !(errType instanceof SQLTesterException)){ - args.unshift(errType); - errType = SQLTesterException; - } throw new errType(...args); }; @@ -429,7 +434,7 @@ class SQLTester { }catch(e){ if(e instanceof SQLTesterException){ threw = true; - this.outln("🔥EXCEPTION: ",''+e); + this.outln("🔥EXCEPTION: ",e); ++this.metrics.nAbortedScript; if( this.#keepGoing ){ this.outln("Continuing anyway becaure of the keep-going option."); @@ -537,7 +542,7 @@ class SQLTester { ); if( 0!==rc ){ if(throwOnError){ - throw new DbException(pDb, rc); + throw new DbException(self, pDb, rc); }else if( sb ){ self.#appendDbErr(db, sb, rc); } @@ -699,7 +704,32 @@ class TestScript { } #checkForDirective(tester,line){ - //todo + 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( ! checkRequiredProperties( tester, rp.split("\\s+") ) ){ + 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){ @@ -734,7 +764,7 @@ class TestScript { this.#outer.verbosity(tester.verbosity()); let line, directive, argv = []; while( null != (line = this.getLine()) ){ - this.verbose3("input line: ",line); + this.verbose3("run() input line: ",line); this.#checkForDirective(tester, line); argv = this.#getCommandArgv(line); if( argv ){ @@ -747,10 +777,10 @@ class TestScript { } #processCommand(tester, argv){ - this.verbose1("running command: ",argv[0], " ", Util.argvToString(argv)); + this.verbose2("processCommand(): ",argv[0], " ", Util.argvToString(argv)); if(this.#outer.verbosity()>1){ const input = tester.getInputText(); - if( !!input ) this.verbose3("Input buffer = ",input); + this.verbose3("processCommand() input buffer = ",input); } CommandDispatcher.dispatch(tester, this, argv); } @@ -1084,7 +1114,7 @@ class TestCaseCommand extends Command { //! --verbosity command class VerbosityCommand extends Command { process(t, ts, argv){ - t.argcCheck(ts,argv,1); + this.argcCheck(ts,argv,1); ts.verbosity( parseInt(argv[1]) ); } } @@ -1123,7 +1153,7 @@ class CommandDispatcher { static dispatch(tester, testScript, argv){ const cmd = CommandDispatcher.getCommandByName(argv[0]); if( !cmd ){ - toss(UnknownCommand,argv[0],' ',testScript.getOutputPrefix()); + toss(UnknownCommand,testScript,argv[0]); } cmd.process(tester, testScript, argv); } -- cgit v1.2.3 From ac5e1f82ce5e4103bf31882252587f3cbb2a564b Mon Sep 17 00:00:00 2001 From: stephan Date: Wed, 30 Aug 2023 00:22:54 +0000 Subject: Add a mechanism with which to import external SQLTester scripts into the JS testing tool. FossilOrigin-Name: bb08ba020ce1d86ca6aa92f43d5ae915f67d08fa73120e1f603d150e76166624 --- ext/wasm/SQLTester/SQLTester.mjs | 81 ++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 12 deletions(-) (limited to 'ext/wasm/SQLTester/SQLTester.mjs') diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs index aa02d7a5e..0ec82ca46 100644 --- a/ext/wasm/SQLTester/SQLTester.mjs +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -9,12 +9,13 @@ ** 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 file contains the main application entry pointer for the JS +** implementation of the SQLTester framework. This version is not well +** documented because the one it's a direct port of is documented: +** in the main SQLite3 source tree, see +** ext/jni/src/org/sqlite/jni/tester/SQLite3Tester.java. */ -// UNDER CONSTRUCTION. Still being ported from the Java impl. - import sqlite3ApiInit from '/jswasm/sqlite3.mjs'; const sqlite3 = await sqlite3ApiInit(); @@ -23,6 +24,43 @@ 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( 1 ){ + // Try OPFS storage, if available... + 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); + }); + } + if(sqlite3.oo1.OpfsDb){ + tryInstallVfs("opfs"); + } +} + +const wPost = (type,...payload)=>{ + postMessage({type, payload}); +}; + // Return a new enum entry value const newE = ()=>Object.create(null); @@ -215,7 +253,7 @@ class SQLTester { //! Test result buffer. #resultBuffer = []; //! Output representation of SQL NULL. - #nullView = "nil"; + #nullView; metrics = newObj({ //! Total tests run nTotalTest: 0, @@ -245,6 +283,7 @@ class SQLTester { }); constructor(){ + this.reset(); } outln(...args){ return this.#outer.outln(...args); } @@ -261,7 +300,7 @@ class SQLTester { this.#clearBuffer(this.#db.initSql); this.closeAllDbs(); this.metrics.nTest = 0; - this.nullView = "nil"; + this.#nullView = "nil"; this.emitColNames = false; this.#db.iCurrentDb = 0; //this.#db.initSql.push("SELECT 1;"); @@ -424,12 +463,19 @@ class SQLTester { runTests(){ const tStart = (new Date()).getTime(); + let isVerbose = this.verbosity(); for(const ts of this.#aScripts){ this.reset(); ++this.metrics.nTestFile; let threw = false; const timeStart = (new Date()).getTime(); + let msgTail = ''; try{ + if( isVerbose ){ + this.#outer.verbose1("Running ",ts.filename()); + }else{ + msgTail = ' '+ts.filename(); + } ts.run(this); }catch(e){ if(e instanceof SQLTesterException){ @@ -446,7 +492,7 @@ class SQLTester { }finally{ const timeEnd = (new Date()).getTime(); this.outln("🏁", (threw ? "❌" : "✅"), " ", this.metrics.nTest, - " test(s) in ", (timeEnd-timeStart),"ms."); + " test(s) in ", (timeEnd-timeStart),"ms.",msgTail); } } const tEnd = (new Date()).getTime(); @@ -544,7 +590,7 @@ class SQLTester { if(throwOnError){ throw new DbException(self, pDb, rc); }else if( sb ){ - self.#appendDbErr(db, sb, rc); + self.#appendDbErr(pDb, sb, rc); } break; } @@ -666,13 +712,20 @@ class TestScript { if( 2 == args.length ){ filename = args[0]; content = args[1]; - }else{ - content = args[0]; + }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){ + }else if((content instanceof ArrayBuffer) + ||(content instanceof Array)){ content = new Uint8Array(content); }else{ toss(Error, "Invalid content type for TestScript constructor."); @@ -686,6 +739,10 @@ class TestScript { 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 || this.#filename)+"]"; @@ -1041,7 +1098,7 @@ class RunCommand extends Command { 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,": ", + ts.verbose2(argv[0]," non-fatal command error #",rc,": ", msg,"\nfor SQL:\n",sql); } } -- cgit v1.2.3 From e621556724c044aa09e7c4fdab4e8407df83d216 Mon Sep 17 00:00:00 2001 From: stephan Date: Wed, 30 Aug 2023 11:54:43 +0000 Subject: Add a UI, of sorts, to the JS SQLTester. FossilOrigin-Name: 249e82b9917ea47c56ee1cbd3345a977d335fd3fc0d67a1ef157813ef4571c7c --- ext/wasm/SQLTester/SQLTester.mjs | 165 +++++++++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 51 deletions(-) (limited to 'ext/wasm/SQLTester/SQLTester.mjs') diff --git a/ext/wasm/SQLTester/SQLTester.mjs b/ext/wasm/SQLTester/SQLTester.mjs index 0ec82ca46..895c64611 100644 --- a/ext/wasm/SQLTester/SQLTester.mjs +++ b/ext/wasm/SQLTester/SQLTester.mjs @@ -10,10 +10,11 @@ ** ************************************************************************* ** This file contains the main application entry pointer for the JS -** implementation of the SQLTester framework. This version is not well -** documented because the one it's a direct port of is documented: -** in the main SQLite3 source tree, see -** ext/jni/src/org/sqlite/jni/tester/SQLite3Tester.java. +** 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'; @@ -24,6 +25,12 @@ 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); @@ -39,9 +46,13 @@ const tryInstallVfs = function f(vfsName){ }; tryInstallVfs.vfsName = undefined; -if( 1 ){ +if( 0 && globalThis.WorkerGlobalScope ){ // Try OPFS storage, if available... - if(sqlite3.installOpfsSAHPoolVfs){ + if( 0 && sqlite3.oo1.OpfsDb ){ + /* Really slow with these tests */ + tryInstallVfs("opfs"); + } + if( sqlite3.installOpfsSAHPoolVfs ){ await sqlite3.installOpfsSAHPoolVfs({ clearOnInit: true, initialCapacity: 15, @@ -52,14 +63,16 @@ if( 1 ){ log("OpfsSAHPool could not load:",e); }); } - if(sqlite3.oo1.OpfsDb){ - tryInstallVfs("opfs"); - } } -const wPost = (type,...payload)=>{ - postMessage({type, payload}); -}; +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); @@ -99,6 +112,12 @@ class SQLTesterException extends globalThis.Error { } this.name = 'SQLTesterException'; } + /** + If this overrideable method returns false (the default) then + exceptions of that type are fatal to a whole test run, instead of + just the test which triggered it. If the the "keep going" flag + is set, this preference is ignored. + */ isFatal() { return false; } } @@ -107,7 +126,7 @@ SQLTesterException.toss = (...args)=>{ } class DbException extends SQLTesterException { - constructor(testScript, pDb, rc, closeDb){ + 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); @@ -137,12 +156,14 @@ class IncompatibleDirective extends SQLTesterException { } } +//! 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; @@ -190,26 +211,41 @@ class Outer { #verbosity = 0; #logger = console.log.bind(console); - constructor(){ + 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.#lnBuf.length && this.getOutputPrefix ){ + if( this.getOutputPrefix && !this.#lnBuf.length ){ this.#lnBuf.push(this.getOutputPrefix()); } this.#lnBuf.push(...args); return this; } - outln(...args){ - if(!this.#lnBuf.length && this.getOutputPrefix ){ + + #outlnImpl(vLevel, ...args){ + if( this.getOutputPrefix && !this.#lnBuf.length ){ this.#lnBuf.push(this.getOutputPrefix()); } this.#lnBuf.push(...args,'\n'); - this.#logger(this.#lnBuf.join('')); + 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 @@ -220,9 +256,9 @@ class Outer { } } - verboseN(lvl, argv){ + verboseN(lvl, args){ if( this.#verbosity>=lvl ){ - this.outln('VERBOSE ',lvl,': ',...argv); + this.#outlnImpl(lvl,'VERBOSE ',lvl,': ',...args); } } verbose1(...args){ return this.verboseN(1,args); } @@ -230,13 +266,8 @@ class Outer { verbose3(...args){ return this.verboseN(3,args); } verbosity(){ - let rc; - if(arguments.length){ - rc = this.#verbosity; - this.#verbosity = arguments[0]; - }else{ - rc = this.#verbosity; - } + const rc = this.#verbosity; + if(arguments.length) this.#verbosity = +arguments[0]; return rc; } @@ -261,7 +292,7 @@ class SQLTester { nTestFile: 0, //! Number of scripts which were aborted nAbortedScript: 0, - //! Incremented by test case handlers + //! Test-case count for to the current TestScript nTest: 0 }); #emitColNames = false; @@ -288,6 +319,24 @@ class SQLTester { 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; @@ -339,8 +388,6 @@ class SQLTester { return this.#takeBuffer(this.#resultBuffer); } - verbosity(...args){ return this.#outer.verbosity(...args); } - nullValue(){ if( 0==arguments.length ){ return this.#nullView; @@ -427,7 +474,7 @@ class SQLTester { pDb = wasm.peekPtr(ppOut); }); if( 0==rc && this.#db.initSql.length > 0){ - //this.#outer.verbose2("RUNNING DB INIT CODE: ",this.#db.initSql.toString()); + this.#outer.verbose2("RUNNING DB INIT CODE: ",this.#db.initSql.toString()); rc = this.execSql(pDb, false, ResultBufferMode.NONE, null, this.#db.initSql.join('')); } @@ -464,6 +511,8 @@ class SQLTester { runTests(){ const tStart = (new Date()).getTime(); let isVerbose = this.verbosity(); + this.metrics.nAbortedScript = 0; + this.metrics.nTotalTest = 0; for(const ts of this.#aScripts){ this.reset(); ++this.metrics.nTestFile; @@ -471,11 +520,6 @@ class SQLTester { const timeStart = (new Date()).getTime(); let msgTail = ''; try{ - if( isVerbose ){ - this.#outer.verbose1("Running ",ts.filename()); - }else{ - msgTail = ' '+ts.filename(); - } ts.run(this); }catch(e){ if(e instanceof SQLTesterException){ @@ -483,21 +527,35 @@ class SQLTester { this.outln("🔥EXCEPTION: ",e); ++this.metrics.nAbortedScript; if( this.#keepGoing ){ - this.outln("Continuing anyway becaure of the keep-going option."); + this.outln("Continuing anyway because of the keep-going option."); + }else if( e.isFatal() ){ + throw e; } - else if( e.isFatal() ) throw e; }else{ throw e; } }finally{ const timeEnd = (new Date()).getTime(); - this.outln("🏁", (threw ? "❌" : "✅"), " ", this.metrics.nTest, - " test(s) in ", (timeEnd-timeStart),"ms.",msgTail); + 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(); - this.outln("Total run-time: ",(tEnd-tStart),"ms"); Util.unlink(this.#db.initialDbName); + this.outln("Took ",(tEnd-tStart),"ms. test count = ", + this.metrics.nTotalTest,", script count = ", + this.#aScripts.length,( + this.metrics.nAbortedScript + ? ", aborted scripts = "+this.metrics.nAbortedScript + : "" + ) + ); return this; } @@ -511,10 +569,6 @@ class SQLTester { } } - /** - Returns v or some escaped form of v, as defined in the tester's - spec doc. - */ #escapeSqlValue(v){ if( !v ) return "{}"; if( !Rx.special.test(v) ){ @@ -735,6 +789,11 @@ class TestScript { 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]); @@ -760,6 +819,14 @@ class TestScript { 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; + } + #checkForDirective(tester,line){ if(line.startsWith("#")){ throw new IncompatibleDirective(this, "C-preprocessor input: "+line); @@ -818,7 +885,8 @@ class TestScript { run(tester){ this.reset(); - this.#outer.verbosity(tester.verbosity()); + 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); @@ -942,11 +1010,6 @@ class TestScript { cur.lineNo = cur.putbackLineNo; } - 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){ return this.#outer.verbosity(...args); } - }/*TestScript*/; //! --close command -- cgit v1.2.3