4 ** The author disclaims copyright to this source code. In place of
5 ** a legal notice, here is a blessing:
7 ** May you do good and not evil.
8 ** May you find forgiveness for yourself and forgive others.
9 ** May you share freely, never taking more than you give.
11 *************************************************************************
12 ** This file contains the main application entry pointer for the JS
13 ** implementation of the SQLTester framework.
15 ** This version is not well-documented because it's a direct port of
16 ** the Java immplementation, which is documented: in the main SQLite3
17 ** source tree, see ext/jni/src/org/sqlite/jni/tester/SQLite3Tester.java.
20 import sqlite3ApiInit from '/jswasm/sqlite3.mjs';
22 const sqlite3 = await sqlite3ApiInit();
24 const log = (...args)=>{
25 console.log('SQLTester:',...args);
29 Try to install vfsName as the new default VFS. Once this succeeds
30 (returns true) then it becomes a no-op on future calls. Throws if
31 vfs registration as the default VFS fails but has no side effects
32 if vfsName is not currently registered.
34 const tryInstallVfs = function f(vfsName){
35 if(f.vfsName) return false;
36 const pVfs = sqlite3.capi.sqlite3_vfs_find(vfsName);
38 log("Installing",'"'+vfsName+'"',"as default VFS.");
39 const rc = sqlite3.capi.sqlite3_vfs_register(pVfs, 1);
41 sqlite3.SQLite3Error.toss(rc,"While trying to register",vfsName,"vfs.");
47 tryInstallVfs.vfsName = undefined;
49 if( 0 && globalThis.WorkerGlobalScope ){
50 // Try OPFS storage, if available...
51 if( 0 && sqlite3.oo1.OpfsDb ){
52 /* Really slow with these tests */
53 tryInstallVfs("opfs");
55 if( sqlite3.installOpfsSAHPoolVfs ){
56 await sqlite3.installOpfsSAHPoolVfs({
59 name: 'opfs-SQLTester'
61 tryInstallVfs(pool.vfsName);
63 log("OpfsSAHPool could not load:",e);
68 const wPost = (function(){
69 return (('undefined'===typeof WorkerGlobalScope)
72 postMessage({type, payload});
75 //log("WorkerGlobalScope",globalThis.WorkerGlobalScope);
77 // Return a new enum entry value
78 const newE = ()=>Object.create(null);
80 const newObj = (props)=>Object.assign(newE(), props);
83 Modes for how to escape (or not) column values and names from
84 SQLTester.execSql() to the result buffer output.
86 const ResultBufferMode = Object.assign(Object.create(null),{
87 //! Do not append to result buffer
89 //! Append output escaped.
91 //! Append output as-is
96 Modes to specify how to emit multi-row output from
97 SQLTester.execSql() to the result buffer.
99 const ResultRowMode = newObj({
100 //! Keep all result rows on one line, space-separated.
102 //! Add a newline between each result row.
106 class SQLTesterException extends globalThis.Error {
107 constructor(testScript, ...args){
109 super( [testScript.getOutputPrefix()+": ", ...args].join('') );
111 super( args.join('') );
113 this.name = 'SQLTesterException';
115 isFatal() { return false; }
118 SQLTesterException.toss = (...args)=>{
119 throw new SQLTesterException(...args);
122 class DbException extends SQLTesterException {
123 constructor(testScript, pDb, rc, closeDb=false){
124 super(testScript, "DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb));
125 this.name = 'DbException';
126 if( closeDb ) sqlite3.capi.sqlite3_close_v2(pDb);
128 isFatal() { return true; }
131 class TestScriptFailed extends SQLTesterException {
132 constructor(testScript, ...args){
133 super(testScript,...args);
134 this.name = 'TestScriptFailed';
136 isFatal() { return true; }
139 class UnknownCommand extends SQLTesterException {
140 constructor(testScript, cmdName){
141 super(testScript, cmdName);
142 this.name = 'UnknownCommand';
144 isFatal() { return true; }
147 class IncompatibleDirective extends SQLTesterException {
148 constructor(testScript, ...args){
149 super(testScript,...args);
150 this.name = 'IncompatibleDirective';
154 //! For throwing where an expression is required.
155 const toss = (errType, ...args)=>{
156 throw new errType(...args);
159 const __utf8Decoder = new TextDecoder();
160 const __utf8Encoder = new TextEncoder('utf-8');
161 //! Workaround for Util.utf8Decode()
162 const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer)
163 ? function(){} : globalThis.SharedArrayBuffer;
166 /* Frequently-reused regexes. */
168 requiredProperties: / REQUIRED_PROPERTIES:[ \t]*(\S.*)\s*$/,
169 scriptModuleName: / SCRIPT_MODULE_NAME:[ \t]*(\S+)\s*$/,
170 mixedModuleName: / ((MIXED_)?MODULE_NAME):[ \t]*(\S+)\s*$/,
171 command: /^--(([a-z-]+)( .*)?)$/,
172 //! "Special" characters - we have to escape output if it contains any.
173 special: /[\x00-\x20\x22\x5c\x7b\x7d]/,
179 const Util = newObj({
182 unlink: function f(fn){
184 f.unlink = sqlite3.wasm.xWrap('sqlite3__wasm_vfs_unlink','int',
187 return 0==f.unlink(0,fn);
190 argvToString: (list)=>{
192 m.shift() /* strip command name */;
196 utf8Decode: function(arrayBuffer, begin, end){
197 return __utf8Decoder.decode(
198 (arrayBuffer.buffer instanceof __SAB)
199 ? arrayBuffer.slice(begin, end)
200 : arrayBuffer.subarray(begin, end)
204 utf8Encode: (str)=>__utf8Encoder.encode(str),
206 strglob: sqlite3.wasm.xWrap('sqlite3__wasm_SQLTester_strglob','int',
213 #logger = console.log.bind(console);
216 if(func) this.setFunc(func);
221 this.#logger = args[0];
228 if( this.getOutputPrefix && !this.#lnBuf.length ){
229 this.#lnBuf.push(this.getOutputPrefix());
231 this.#lnBuf.push(...args);
235 #outlnImpl(vLevel, ...args){
236 if( this.getOutputPrefix && !this.#lnBuf.length ){
237 this.#lnBuf.push(this.getOutputPrefix());
239 this.#lnBuf.push(...args,'\n');
240 const msg = this.#lnBuf.join('');
241 this.#lnBuf.length = 0;
247 return this.#outlnImpl(0,...args);
251 if( 0==arguments.length ){
252 return (this.getOutputPrefix
253 ? (this.getOutputPrefix() ?? '') : '');
255 this.getOutputPrefix = arguments[0];
260 static #verboseLabel = ["🔈",/*"🔉",*/"🔊","📢"];
262 if( this.#verbosity>=lvl ){
263 this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
266 verbose1(...args){ return this.verboseN(1,args); }
267 verbose2(...args){ return this.verboseN(2,args); }
268 verbose3(...args){ return this.verboseN(3,args); }
271 const rc = this.#verbosity;
272 if(arguments.length) this.#verbosity = +arguments[0];
280 //! Console output utility.
281 #outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
282 //! List of input scripts.
284 //! Test input buffer.
286 //! Test result buffer.
288 //! Output representation of SQL NULL.
293 //! Total test script files run
295 //! Test-case count for to the current TestScript
297 //! Names of scripts which were aborted.
300 #emitColNames = false;
301 //! True to keep going regardless of how a test fails.
304 //! The list of available db handles.
306 //! Index into this.list of the current db.
308 //! Name of the default db, re-created for each script.
309 initialDbName: "test.db",
310 //! Buffer for REQUIRED_PROPERTIES pragmas.
311 initSql: ['select 1;'],
312 //! (sqlite3*) to the current db.
313 currentDb: function(){
314 return this.list[this.iCurrentDb];
322 outln(...args){ return this.#outer.outln(...args); }
323 out(...args){ return this.#outer.out(...args); }
326 this.#outer = args[0];
331 verbose1(...args){ return this.#outer.verboseN(1,args); }
332 verbose2(...args){ return this.#outer.verboseN(2,args); }
333 verbose3(...args){ return this.#outer.verboseN(3,args); }
335 const rc = this.#outer.verbosity(...args);
336 return args.length ? this : rc;
339 this.#outer.logger(func);
343 incrementTestCounter(){
344 ++this.metrics.nTotalTest;
345 ++this.metrics.nTest;
349 this.clearInputBuffer();
350 this.clearResultBuffer();
351 this.#clearBuffer(this.#db.initSql);
353 this.metrics.nTest = 0;
354 this.#nullView = "nil";
355 this.emitColNames = false;
356 this.#db.iCurrentDb = 0;
357 //this.#db.initSql.push("SELECT 1;");
360 appendInput(line, addNL){
361 this.#inputBuffer.push(line);
362 if( addNL ) this.#inputBuffer.push('\n');
364 appendResult(line, addNL){
365 this.#resultBuffer.push(line);
366 if( addNL ) this.#resultBuffer.push('\n');
368 appendDbInitSql(sql){
369 this.#db.initSql.push(sql);
370 if( this.currentDb() ){
371 this.execSql(null, true, ResultBufferMode.NONE, null, sql);
377 for(const sql of this.#db.initSql){
378 this.#outer.verbose2("RUNNING DB INIT CODE: ",sql);
379 rc = this.execSql(pDb, false, ResultBufferMode.NONE, null, sql);
385 #clearBuffer(buffer){
390 clearInputBuffer(){ return this.#clearBuffer(this.#inputBuffer); }
391 clearResultBuffer(){return this.#clearBuffer(this.#resultBuffer); }
393 getInputText(){ return this.#inputBuffer.join(''); }
394 getResultText(){ return this.#resultBuffer.join(''); }
397 const s = buffer.join('');
403 return this.#takeBuffer(this.#inputBuffer);
406 return this.#takeBuffer(this.#resultBuffer);
410 return (0==arguments.length)
412 : (this.#nullView = ''+arguments[0]);
416 return (0==arguments.length)
418 : (this.#emitColNames = !!arguments[0]);
422 return (0==arguments.length)
423 ? this.#db.iCurrentDb
424 : (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
428 if(id<0 || id>=this.#db.list.length){
429 toss(SQLTesterException, "Database index ",id," is out of range.");
435 if( 0!=args.length ){
436 this.#affirmDbId(id).#db.iCurrentDb = id;
438 return this.#db.currentDb();
442 return this.#affirmDbId(id).#db.list[id];
445 getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
449 if( 0==arguments.length ){
450 id = this.#db.iCurrentDb;
452 const pDb = this.#affirmDbId(id).#db.list[id];
454 sqlite3.capi.sqlite3_close_v2(pDb);
455 this.#db.list[id] = null;
460 for(let i = 0; i<this.#db.list.length; ++i){
461 if(this.#db.list[i]){
462 sqlite3.capi.sqlite3_close_v2(this.#db.list[i]);
463 this.#db.list[i] = null;
466 this.#db.iCurrentDb = 0;
469 openDb(name, createIfNeeded){
470 if( 3===arguments.length ){
471 const slot = arguments[0];
472 this.#affirmDbId(slot).#db.iCurrentDb = slot;
474 createIfNeeded = arguments[2];
477 const capi = sqlite3.capi, wasm = sqlite3.wasm;
479 let flags = capi.SQLITE_OPEN_READWRITE;
480 if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
483 wasm.pstack.call(function(){
484 let ppOut = wasm.pstack.allocPtr();
485 rc = sqlite3.capi.sqlite3_open_v2(name, ppOut, flags, null);
486 pDb = wasm.peekPtr(ppOut);
489 if( 0==rc && this.#db.initSql.length > 0){
490 rc = this.#runInitSql(pDb);
493 sqlite3.SQLite3Error.toss(
495 "sqlite3 result code",rc+":",
496 (pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
497 : sqlite3.capi.sqlite3_errstr(rc))
500 return this.#db.list[this.#db.iCurrentDb] = pDb;
502 sqlite3.capi.sqlite3_close_v2(pDb);
508 if( 2===arguments.length ){
509 ts = new TestScript(arguments[0], arguments[1]);
510 }else if(ts instanceof Uint8Array){
511 ts = new TestScript('<unnamed>', ts);
512 }else if('string' === typeof arguments[1]){
513 ts = new TestScript('<unnamed>', Util.utf8Encode(arguments[1]));
515 if( !(ts instanceof TestScript) ){
516 Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
518 this.#aScripts.push(ts);
523 const tStart = (new Date()).getTime();
524 let isVerbose = this.verbosity();
525 this.metrics.failedScripts.length = 0;
526 this.metrics.nTotalTest = 0;
527 this.metrics.nTestFile = 0;
528 for(const ts of this.#aScripts){
530 ++this.metrics.nTestFile;
532 const timeStart = (new Date()).getTime();
537 if(e instanceof SQLTesterException){
539 this.outln("🔥EXCEPTION: ",e);
540 this.metrics.failedScripts.push({script: ts.filename(), message:e.toString()});
541 if( this.#keepGoing ){
542 this.outln("Continuing anyway because of the keep-going option.");
543 }else if( e.isFatal() ){
550 const timeEnd = (new Date()).getTime();
551 this.out("🏁", (threw ? "❌" : "✅"), " ",
552 this.metrics.nTest, " test(s) in ",
553 (timeEnd-timeStart),"ms. ");
554 const mod = ts.moduleName();
556 this.out( "[",mod,"] " );
558 this.outln(ts.filename());
561 const tEnd = (new Date()).getTime();
562 Util.unlink(this.#db.initialDbName);
563 this.outln("Took ",(tEnd-tStart),"ms. Test count = ",
564 this.metrics.nTotalTest,", script count = ",
565 this.#aScripts.length,(
566 this.metrics.failedScripts.length
567 ? ", failed scripts = "+this.metrics.failedScripts.length
575 if( !this.#db.list[0] ){
576 Util.unlink(this.#db.initialDbName);
577 this.openDb(0, this.#db.initialDbName, true);
579 this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
580 "triggered while it is opened.");
585 if( !v ) return "{}";
586 if( !Rx.special.test(v) ){
587 return v /* no escaping needed */;
589 if( !Rx.squiggly.test(v) ){
594 for(let i = 0; i < n; ++i){
595 const ch = v.charAt(i);
597 case '\\': sb.push("\\\\"); break;
598 case '"': sb.push("\\\""); break;
600 //verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
601 const ccode = ch.charCodeAt(i);
602 if( ccode < 32 ) sb.push('\\',ccode.toString(8),'o');
612 #appendDbErr(pDb, sb, rc){
613 sb.push(sqlite3.capi.sqlite3_js_rc_str(rc), ' ');
614 const msg = this.#escapeSqlValue(sqlite3.capi.sqlite3_errmsg(pDb));
615 if( '{' === msg.charAt(0) ){
618 sb.push('{', msg, '}');
623 sqlite3.oo1.DB.checkRc(pDb, rc);
626 execSql(pDb, throwOnError, appendMode, rowMode, sql){
627 if( !pDb && !this.#db.list[0] ){
628 this.#setupInitialDb();
630 if( !pDb ) pDb = this.#db.currentDb();
631 const wasm = sqlite3.wasm, capi = sqlite3.capi;
632 sql = (sql instanceof Uint8Array)
634 : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
636 const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
638 wasm.scopedAllocCall(function(){
639 let sqlByteLen = sql.byteLength;
640 const ppStmt = wasm.scopedAlloc(
641 /* output (sqlite3_stmt**) arg and pzTail */
642 (2 * wasm.ptrSizeof) + (sqlByteLen + 1/* SQL + NUL */)
644 const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
645 let pSql = pzTail + wasm.ptrSizeof;
646 const pSqlEnd = pSql + sqlByteLen;
647 wasm.heap8().set(sql, pSql);
648 wasm.poke8(pSql + sqlByteLen, 0/*NUL terminator*/);
649 let pos = 0, n = 1, spacing = 0;
650 while( pSql && wasm.peek8(pSql) ){
651 wasm.pokePtr([ppStmt, pzTail], 0);
652 rc = capi.sqlite3_prepare_v3(
653 pDb, pSql, sqlByteLen, 0, ppStmt, pzTail
657 throw new DbException(self, pDb, rc);
659 self.#appendDbErr(pDb, sb, rc);
663 const pStmt = wasm.peekPtr(ppStmt);
664 pSql = wasm.peekPtr(pzTail);
665 sqlByteLen = pSqlEnd - pSql;
666 if(!pStmt) continue /* only whitespace or comments */;
668 const nCol = capi.sqlite3_column_count(pStmt);
670 while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {
671 for( let i=0; i < nCol; ++i ){
672 if( spacing++ > 0 ) sb.push(' ');
673 if( self.#emitColNames ){
674 colName = capi.sqlite3_column_name(pStmt, i);
676 case ResultBufferMode.ASIS: sb.push( colName ); break;
677 case ResultBufferMode.ESCAPED:
678 sb.push( self.#escapeSqlValue(colName) );
681 self.toss("Unhandled ResultBufferMode.");
685 val = capi.sqlite3_column_text(pStmt, i);
687 sb.push( self.#nullView );
691 case ResultBufferMode.ASIS: sb.push( val ); break;
692 case ResultBufferMode.ESCAPED:
693 sb.push( self.#escapeSqlValue(val) );
698 if( ResultRowMode.NEWLINE === rowMode ){
702 }else{ // no output but possibly other side effects
703 while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
705 capi.sqlite3_finalize(pStmt);
706 if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
709 self.#appendDbErr(db, sb, rc);
713 }/* SQL script loop */;
714 })/*scopedAllocCall()*/;
724 process(sqlTester,testScript,argv){
725 SQLTesterException.toss("process() must be overridden");
728 argcCheck(testScript,argv,min,max){
729 const argc = argv.length-1;
730 if(argc<min || (max>=0 && argc>max)){
732 testScript.toss(argv[0]," requires exactly ",min," argument(s)");
734 testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
736 testScript.toss(argv[0]," requires at least ",min," arguments.");
746 //! Current line number. Starts at 0 for internal reasons and will
747 // line up with 1-based reality once parsing starts.
748 lineNo = 0 /* yes, zero */;
749 //! Putback value for this.pos.
751 //! Putback line number
753 //! Peeked-to pos, used by peekLine() and consumePeeked().
755 //! Peeked-to line number.
761 //! Restore parsing state to the start of the stream.
763 this.sb.length = this.pos = this.lineNo
764 = this.putbackPos = this.putbackLineNo
765 = this.peekedPos = this.peekedLineNo = 0;
770 #cursor = new Cursor();
773 #testCaseName = null;
774 #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
776 constructor(...args){
777 let content, filename;
778 if( 2 == args.length ){
781 }else if( 1 == args.length ){
782 if(args[0] instanceof Object){
790 if(!(content instanceof Uint8Array)){
791 if('string' === typeof content){
792 content = Util.utf8Encode(content);
793 }else if((content instanceof ArrayBuffer)
794 ||(content instanceof Array)){
795 content = new Uint8Array(content);
797 toss(Error, "Invalid content type for TestScript constructor.");
800 this.#filename = filename;
801 this.#cursor.src = content;
805 return (0==arguments.length)
806 ? this.#moduleName : (this.#moduleName = arguments[0]);
810 return (0==arguments.length)
811 ? this.#testCaseName : (this.#testCaseName = arguments[0]);
814 return (0==arguments.length)
815 ? this.#filename : (this.#filename = arguments[0]);
819 let rc = "["+(this.#moduleName || '<unnamed>')+"]";
820 if( this.#testCaseName ) rc += "["+this.#testCaseName+"]";
821 if( this.#filename ) rc += '['+this.#filename+']';
822 return rc + " line "+ this.#cursor.lineNo;
826 this.#testCaseName = null;
827 this.#cursor.rewind();
832 throw new TestScriptFailed(this,...args);
835 verbose1(...args){ return this.#outer.verboseN(1,args); }
836 verbose2(...args){ return this.#outer.verboseN(2,args); }
837 verbose3(...args){ return this.#outer.verboseN(3,args); }
839 const rc = this.#outer.verbosity(...args);
840 return args.length ? this : rc;
843 #checkRequiredProperties(tester, props){
844 if(true) return false;
846 for(const rp of props){
847 this.verbose2("REQUIRED_PROPERTIES: ",rp);
849 case "RECURSIVE_TRIGGERS":
850 tester.appendDbInitSql("pragma recursive_triggers=on;");
853 case "TEMPSTORE_FILE":
854 /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
855 which we just happen to know is the case */
856 tester.appendDbInitSql("pragma temp_store=1;");
859 case "TEMPSTORE_MEM":
860 /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
861 which we just happen to know is the case */
862 tester.appendDbInitSql("pragma temp_store=0;");
866 tester.appendDbInitSql("pragma auto_vacuum=full;");
870 tester.appendDbInitSql("pragma auto_vacuum=incremental;");
876 return props.length == nOk;
879 #checkForDirective(tester,line){
880 if(line.startsWith("#")){
881 throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
882 }else if(line.startsWith("---")){
883 throw new IncompatibleDirective(this, "triple-dash: ",line);
885 let m = Rx.scriptModuleName.exec(line);
887 this.#moduleName = m[1];
890 m = Rx.requiredProperties.exec(line);
893 if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
894 throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
898 m = Rx.mixedModuleName.exec(line);
900 throw new IncompatibleDirective(this, m[1]+": "+m[3]);
902 if( line.indexOf("\n|")>=0 ){
903 throw new IncompatibleDirective(this, "newline-pipe combination.");
908 #getCommandArgv(line){
909 const m = Rx.command.exec(line);
910 return m ? m[1].trim().split(/\s+/) : null;
914 #isCommandLine(line, checkForImpl){
915 let m = Rx.command.exec(line);
916 if( m && checkForImpl ){
917 m = !!CommandDispatcher.getCommandByName(m[2]);
922 fetchCommandBody(tester){
925 while( (null !== (line = this.peekLine())) ){
926 this.#checkForDirective(tester, line);
927 if( this.#isCommandLine(line, true) ) break;
929 this.consumePeeked();
932 return !!line.trim() ? line : null;
937 this.#outer.verbosity( tester.verbosity() );
938 this.#outer.logger( tester.outer().logger() );
939 let line, directive, argv = [];
940 while( null != (line = this.getLine()) ){
941 this.verbose3("run() input line: ",line);
942 this.#checkForDirective(tester, line);
943 argv = this.#getCommandArgv(line);
945 this.#processCommand(tester, argv);
948 tester.appendInput(line,true);
953 #processCommand(tester, argv){
954 this.verbose2("processCommand(): ",argv[0], " ", Util.argvToString(argv));
955 if(this.#outer.verbosity()>1){
956 const input = tester.getInputText();
957 this.verbose3("processCommand() input buffer = ",input);
959 CommandDispatcher.dispatch(tester, this, argv);
963 const cur = this.#cursor;
964 if( cur.pos==cur.src.byteLength ){
967 cur.putbackPos = cur.pos;
968 cur.putbackLineNo = cur.lineNo;
970 let b = 0, prevB = 0, i = cur.pos;
972 let nChar = 0 /* number of bytes in the aChar char */;
973 const end = cur.src.byteLength;
974 for(; i < end && !doBreak; ++i){
977 case 13/*CR*/: continue;
980 if(cur.sb.length>0) doBreak = true;
981 // Else it's an empty string
984 /* Multi-byte chars need to be gathered up and appended at
985 one time so that we can get them as string objects. */
988 case 0xC0: nChar = 2; break;
989 case 0xE0: nChar = 3; break;
990 case 0xF0: nChar = 4; break;
992 if( b > 127 ) this.toss("Invalid character (#"+b+").");
996 cur.sb.push(String.fromCharCode(b));
998 const aChar = [] /* multi-byte char buffer */;
999 for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
1001 Util.utf8Decode( new Uint8Array(aChar) )
1010 const rv = cur.sb.join('');
1011 if( i==cur.src.byteLength && 0==rv.length ){
1012 return null /* EOF */;
1018 Fetches the next line then resets the cursor to its pre-call
1019 state. consumePeeked() can be used to consume this peeked line
1020 without having to re-parse it.
1023 const cur = this.#cursor;
1024 const oldPos = cur.pos;
1025 const oldPB = cur.putbackPos;
1026 const oldPBL = cur.putbackLineNo;
1027 const oldLine = cur.lineNo;
1029 return this.getLine();
1031 cur.peekedPos = cur.pos;
1032 cur.peekedLineNo = cur.lineNo;
1034 cur.lineNo = oldLine;
1035 cur.putbackPos = oldPB;
1036 cur.putbackLineNo = oldPBL;
1042 Only valid after calling peekLine() and before calling getLine().
1043 This places the cursor to the position it would have been at had
1044 the peekLine() had been fetched with getLine().
1047 const cur = this.#cursor;
1048 cur.pos = cur.peekedPos;
1049 cur.lineNo = cur.peekedLineNo;
1053 Restores the cursor to the position it had before the previous
1057 const cur = this.#cursor;
1058 cur.pos = cur.putbackPos;
1059 cur.lineNo = cur.putbackLineNo;
1065 class CloseDbCommand extends Command {
1066 process(t, ts, argv){
1067 this.argcCheck(ts,argv,0,1);
1070 const arg = argv[1];
1071 if( "all" === arg ){
1079 id = t.currentDbId();
1085 //! --column-names command
1086 class ColumnNamesCommand extends Command {
1087 process( st, ts, argv ){
1088 this.argcCheck(ts,argv,1);
1089 st.outputColumnNames( !!parseInt(argv[1]) );
1094 class DbCommand extends Command {
1095 process(t, ts, argv){
1096 this.argcCheck(ts,argv,1);
1097 t.currentDbId( parseInt(argv[1]) );
1102 class GlobCommand extends Command {
1104 constructor(negate=false){
1106 this.#negate = negate;
1109 process(t, ts, argv){
1110 this.argcCheck(ts,argv,1,-1);
1111 t.incrementTestCounter();
1112 const sql = t.takeInputBuffer();
1113 let rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
1114 ResultRowMode.ONELINE, sql);
1115 const result = t.getResultText();
1116 const sArgs = Util.argvToString(argv);
1117 //t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
1118 const glob = Util.argvToString(argv);
1119 rc = Util.strglob(glob, result);
1120 if( (this.#negate && 0===rc) || (!this.#negate && 0!==rc) ){
1121 ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
1126 //! --notglob command
1127 class NotGlobCommand extends GlobCommand {
1128 constructor(){super(true);}
1132 class OpenDbCommand extends Command {
1133 #createIfNeeded = false;
1134 constructor(createIfNeeded=false){
1136 this.#createIfNeeded = createIfNeeded;
1138 process(t, ts, argv){
1139 this.argcCheck(ts,argv,1);
1140 t.openDb(argv[1], this.#createIfNeeded);
1145 class NewDbCommand extends OpenDbCommand {
1146 constructor(){ super(true); }
1149 //! Placeholder dummy/no-op commands
1150 class NoopCommand extends Command {
1151 process(t, ts, argv){}
1155 class NullCommand extends Command {
1156 process(st, ts, argv){
1157 this.argcCheck(ts,argv,1);
1158 st.nullValue( argv[1] );
1163 class PrintCommand extends Command {
1164 process(st, ts, argv){
1165 st.out(ts.getOutputPrefix(),': ');
1166 if( 1==argv.length ){
1167 st.out( st.getInputText() );
1169 st.outln( Util.argvToString(argv) );
1174 //! --result command
1175 class ResultCommand extends Command {
1177 constructor(resultBufferMode = ResultBufferMode.ESCAPED){
1179 this.#bufferMode = resultBufferMode;
1181 process(t, ts, argv){
1182 this.argcCheck(ts,argv,0,-1);
1183 t.incrementTestCounter();
1184 const sql = t.takeInputBuffer();
1185 //ts.verbose2(argv[0]," SQL =\n",sql);
1186 t.execSql(null, false, this.#bufferMode, ResultRowMode.ONELINE, sql);
1187 const result = t.getResultText().trim();
1188 const sArgs = argv.length>1 ? Util.argvToString(argv) : "";
1189 if( result !== sArgs ){
1190 t.outln(argv[0]," FAILED comparison. Result buffer:\n",
1191 result,"\nExpected result:\n",sArgs);
1192 ts.toss(argv[0]+" comparison failed.");
1198 class JsonCommand extends ResultCommand {
1199 constructor(){ super(ResultBufferMode.ASIS); }
1203 class RunCommand extends Command {
1204 process(t, ts, argv){
1205 this.argcCheck(ts,argv,0,1);
1206 const pDb = (1==argv.length)
1207 ? t.currentDb() : t.getDbById( parseInt(argv[1]) );
1208 const sql = t.takeInputBuffer();
1209 const rc = t.execSql(pDb, false, ResultBufferMode.NONE,
1210 ResultRowMode.ONELINE, sql);
1211 if( 0!==rc && t.verbosity()>0 ){
1212 const msg = sqlite3.capi.sqlite3_errmsg(pDb);
1213 ts.verbose2(argv[0]," non-fatal command error #",rc,": ",
1214 msg,"\nfor SQL:\n",sql);
1219 //! --tableresult command
1220 class TableResultCommand extends Command {
1222 constructor(jsonMode=false){
1224 this.#jsonMode = jsonMode;
1226 process(t, ts, argv){
1227 this.argcCheck(ts,argv,0);
1228 t.incrementTestCounter();
1229 let body = ts.fetchCommandBody(t);
1230 if( null===body ) ts.toss("Missing ",argv[0]," body.");
1232 if( !body.endsWith("\n--end") ){
1233 ts.toss(argv[0], " must be terminated with --end\\n");
1235 body = body.substring(0, body.length-6);
1237 const globs = body.split(/\s*\n\s*/);
1238 if( globs.length < 1 ){
1239 ts.toss(argv[0], " requires 1 or more ",
1240 (this.#jsonMode ? "json snippets" : "globs"),".");
1242 const sql = t.takeInputBuffer();
1243 t.execSql(null, true,
1244 this.#jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
1245 ResultRowMode.NEWLINE, sql);
1246 const rbuf = t.getResultText().trim();
1247 const res = rbuf.split(/\r?\n/);
1248 if( res.length !== globs.length ){
1249 ts.toss(argv[0], " failure: input has ", res.length,
1250 " row(s) but expecting ",globs.length);
1252 for(let i = 0; i < res.length; ++i){
1253 const glob = globs[i].replaceAll(/\s+/g," ").trim();
1254 //ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
1255 if( this.#jsonMode ){
1256 if( glob!==res[i] ){
1257 ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
1260 }else if( 0!=Util.strglob(glob, res[i]) ){
1261 ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
1267 //! --json-block command
1268 class JsonBlockCommand extends TableResultCommand {
1269 constructor(){ super(true); }
1272 //! --testcase command
1273 class TestCaseCommand extends Command {
1274 process(tester, script, argv){
1275 this.argcCheck(script, argv,1);
1276 script.testCaseName(argv[1]);
1277 tester.clearResultBuffer();
1278 tester.clearInputBuffer();
1283 //! --verbosity command
1284 class VerbosityCommand extends Command {
1285 process(t, ts, argv){
1286 this.argcCheck(ts,argv,1);
1287 ts.verbosity( parseInt(argv[1]) );
1291 class CommandDispatcher {
1292 static map = newObj();
1294 static getCommandByName(name){
1295 let rv = CommandDispatcher.map[name];
1298 case "close": rv = new CloseDbCommand(); break;
1299 case "column-names": rv = new ColumnNamesCommand(); break;
1300 case "db": rv = new DbCommand(); break;
1301 case "glob": rv = new GlobCommand(); break;
1302 case "json": rv = new JsonCommand(); break;
1303 case "json-block": rv = new JsonBlockCommand(); break;
1304 case "new": rv = new NewDbCommand(); break;
1305 case "notglob": rv = new NotGlobCommand(); break;
1306 case "null": rv = new NullCommand(); break;
1307 case "oom": rv = new NoopCommand(); break;
1308 case "open": rv = new OpenDbCommand(); break;
1309 case "print": rv = new PrintCommand(); break;
1310 case "result": rv = new ResultCommand(); break;
1311 case "run": rv = new RunCommand(); break;
1312 case "tableresult": rv = new TableResultCommand(); break;
1313 case "testcase": rv = new TestCaseCommand(); break;
1314 case "verbosity": rv = new VerbosityCommand(); break;
1317 CommandDispatcher.map[name] = rv;
1322 static dispatch(tester, testScript, argv){
1323 const cmd = CommandDispatcher.getCommandByName(argv[0]);
1325 toss(UnknownCommand,testScript,argv[0]);
1327 cmd.process(tester, testScript, argv);
1329 }/*CommandDispatcher*/
1331 const namespace = newObj({
1334 IncompatibleDirective,
1345 export {namespace as default};