Fixes default log output to console for macOS
[sqlcipher.git] / ext / wasm / SQLTester / SQLTester.mjs
blob71c5d4c5386d649b99205ad806f4af6bb265264c
1 /*
2 ** 2023-08-29
3 **
4 ** The author disclaims copyright to this source code.  In place of
5 ** a legal notice, here is a blessing:
6 **
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);
28 /**
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);
37   if(pVfs){
38     log("Installing",'"'+vfsName+'"',"as default VFS.");
39     const rc = sqlite3.capi.sqlite3_vfs_register(pVfs, 1);
40     if(rc){
41       sqlite3.SQLite3Error.toss(rc,"While trying to register",vfsName,"vfs.");
42     }
43     f.vfsName = vfsName;
44   }
45   return !!pVfs;
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");
54   }
55   if( sqlite3.installOpfsSAHPoolVfs ){
56     await sqlite3.installOpfsSAHPoolVfs({
57       clearOnInit: true,
58       initialCapacity: 15,
59       name: 'opfs-SQLTester'
60     }).then(pool=>{
61       tryInstallVfs(pool.vfsName);
62     }).catch(e=>{
63       log("OpfsSAHPool could not load:",e);
64     });
65   }
68 const wPost = (function(){
69   return (('undefined'===typeof WorkerGlobalScope)
70           ? ()=>{}
71           : (type, payload)=>{
72             postMessage({type, payload});
73           });
74 })();
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);
82 /**
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
88   NONE: newE(),
89   //! Append output escaped.
90   ESCAPED: newE(),
91   //! Append output as-is
92   ASIS: newE()
93 });
95 /**
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.
101   ONLINE: newE(),
102   //! Add a newline between each result row.
103   NEWLINE: newE()
106 class SQLTesterException extends globalThis.Error {
107   constructor(testScript, ...args){
108     if(testScript){
109       super( [testScript.getOutputPrefix()+": ", ...args].join('') );
110     }else{
111       super( args.join('') );
112     }
113     this.name = 'SQLTesterException';
114   }
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);
127   }
128   isFatal() { return true; }
131 class TestScriptFailed extends SQLTesterException {
132   constructor(testScript, ...args){
133     super(testScript,...args);
134     this.name = 'TestScriptFailed';
135   }
136   isFatal() { return true; }
139 class UnknownCommand extends SQLTesterException {
140   constructor(testScript, cmdName){
141     super(testScript, cmdName);
142     this.name = 'UnknownCommand';
143   }
144   isFatal() { return true; }
147 class IncompatibleDirective extends SQLTesterException {
148   constructor(testScript, ...args){
149     super(testScript,...args);
150     this.name = 'IncompatibleDirective';
151   }
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. */
167 const Rx = newObj({
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]/,
174   squiggly: /[{}]/
179 const Util = newObj({
180   toss,
182   unlink: function f(fn){
183     if(!f.unlink){
184       f.unlink = sqlite3.wasm.xWrap('sqlite3__wasm_vfs_unlink','int',
185                                     ['*','string']);
186     }
187     return 0==f.unlink(0,fn);
188   },
190   argvToString: (list)=>{
191     const m = [...list];
192     m.shift() /* strip command name */;
193     return m.join(" ")
194   },
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)
201     );
202   },
204   utf8Encode: (str)=>__utf8Encoder.encode(str),
206   strglob: sqlite3.wasm.xWrap('sqlite3__wasm_SQLTester_strglob','int',
207                               ['string','string'])
208 })/*Util*/;
210 class Outer {
211   #lnBuf = [];
212   #verbosity = 0;
213   #logger = console.log.bind(console);
215   constructor(func){
216     if(func) this.setFunc(func);
217   }
219   logger(...args){
220     if(args.length){
221       this.#logger = args[0];
222       return this;
223     }
224     return this.#logger;
225   }
227   out(...args){
228     if( this.getOutputPrefix && !this.#lnBuf.length ){
229       this.#lnBuf.push(this.getOutputPrefix());
230     }
231     this.#lnBuf.push(...args);
232     return this;
233   }
235   #outlnImpl(vLevel, ...args){
236     if( this.getOutputPrefix && !this.#lnBuf.length ){
237       this.#lnBuf.push(this.getOutputPrefix());
238     }
239     this.#lnBuf.push(...args,'\n');
240     const msg = this.#lnBuf.join('');
241     this.#lnBuf.length = 0;
242     this.#logger(msg);
243     return this;
244   }
246   outln(...args){
247     return this.#outlnImpl(0,...args);
248   }
250   outputPrefix(){
251     if( 0==arguments.length ){
252       return (this.getOutputPrefix
253               ? (this.getOutputPrefix() ?? '') : '');
254     }else{
255       this.getOutputPrefix = arguments[0];
256       return this;
257     }
258   }
260   static #verboseLabel = ["🔈",/*"🔉",*/"🔊","📢"];
261   verboseN(lvl, args){
262     if( this.#verbosity>=lvl ){
263       this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
264     }
265   }
266   verbose1(...args){ return this.verboseN(1,args); }
267   verbose2(...args){ return this.verboseN(2,args); }
268   verbose3(...args){ return this.verboseN(3,args); }
270   verbosity(){
271     const rc = this.#verbosity;
272     if(arguments.length) this.#verbosity = +arguments[0];
273     return rc;
274   }
276 }/*Outer*/
278 class SQLTester {
280   //! Console output utility.
281   #outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
282   //! List of input scripts.
283   #aScripts = [];
284   //! Test input buffer.
285   #inputBuffer = [];
286   //! Test result buffer.
287   #resultBuffer = [];
288   //! Output representation of SQL NULL.
289   #nullView;
290   metrics = newObj({
291     //! Total tests run
292     nTotalTest: 0,
293     //! Total test script files run
294     nTestFile: 0,
295     //! Test-case count for to the current TestScript
296     nTest: 0,
297     //! Names of scripts which were aborted.
298     failedScripts: []
299   });
300   #emitColNames = false;
301   //! True to keep going regardless of how a test fails.
302   #keepGoing = false;
303   #db = newObj({
304     //! The list of available db handles.
305     list: new Array(7),
306     //! Index into this.list of the current db.
307     iCurrentDb: 0,
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];
315     }
316   });
318   constructor(){
319     this.reset();
320   }
322   outln(...args){ return this.#outer.outln(...args); }
323   out(...args){ return this.#outer.out(...args); }
324   outer(...args){
325     if(args.length){
326       this.#outer = args[0];
327       return this;
328     }
329     return this.#outer;
330   }
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); }
334   verbosity(...args){
335     const rc = this.#outer.verbosity(...args);
336     return args.length ? this : rc;
337   }
338   setLogger(func){
339     this.#outer.logger(func);
340     return this;
341   }
343   incrementTestCounter(){
344     ++this.metrics.nTotalTest;
345     ++this.metrics.nTest;
346   }
348   reset(){
349     this.clearInputBuffer();
350     this.clearResultBuffer();
351     this.#clearBuffer(this.#db.initSql);
352     this.closeAllDbs();
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;");
358   }
360   appendInput(line, addNL){
361     this.#inputBuffer.push(line);
362     if( addNL ) this.#inputBuffer.push('\n');
363   }
364   appendResult(line, addNL){
365     this.#resultBuffer.push(line);
366     if( addNL ) this.#resultBuffer.push('\n');
367   }
368   appendDbInitSql(sql){
369     this.#db.initSql.push(sql);
370     if( this.currentDb() ){
371       this.execSql(null, true, ResultBufferMode.NONE, null, sql);
372     }
373   }
375   #runInitSql(pDb){
376     let rc = 0;
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);
380       if( rc ) break;
381     }
382     return rc;
383   }
385 #clearBuffer(buffer){
386     buffer.length = 0;
387     return buffer;
388   }
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(''); }
396   #takeBuffer(buffer){
397     const s = buffer.join('');
398     buffer.length = 0;
399     return s;
400   }
402   takeInputBuffer(){
403     return this.#takeBuffer(this.#inputBuffer);
404   }
405   takeResultBuffer(){
406     return this.#takeBuffer(this.#resultBuffer);
407   }
409   nullValue(){
410     return (0==arguments.length)
411       ? this.#nullView
412       : (this.#nullView = ''+arguments[0]);
413   }
415   outputColumnNames(){
416     return (0==arguments.length)
417       ? this.#emitColNames
418       : (this.#emitColNames = !!arguments[0]);
419   }
421   currentDbId(){
422     return (0==arguments.length)
423       ? this.#db.iCurrentDb
424       : (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
425   }
427   #affirmDbId(id){
428     if(id<0 || id>=this.#db.list.length){
429       toss(SQLTesterException, "Database index ",id," is out of range.");
430     }
431     return this;
432   }
434   currentDb(...args){
435     if( 0!=args.length ){
436       this.#affirmDbId(id).#db.iCurrentDb = id;
437     }
438     return this.#db.currentDb();
439   }
441   getDbById(id){
442     return this.#affirmDbId(id).#db.list[id];
443   }
445   getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
448   closeDb(id) {
449     if( 0==arguments.length ){
450       id = this.#db.iCurrentDb;
451     }
452     const pDb = this.#affirmDbId(id).#db.list[id];
453     if( pDb ){
454       sqlite3.capi.sqlite3_close_v2(pDb);
455       this.#db.list[id] = null;
456     }
457   }
459   closeAllDbs(){
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;
464       }
465     }
466     this.#db.iCurrentDb = 0;
467   }
469   openDb(name, createIfNeeded){
470     if( 3===arguments.length ){
471       const slot = arguments[0];
472       this.#affirmDbId(slot).#db.iCurrentDb = slot;
473       name = arguments[1];
474       createIfNeeded = arguments[2];
475     }
476     this.closeDb();
477     const capi = sqlite3.capi, wasm = sqlite3.wasm;
478     let pDb = 0;
479     let flags = capi.SQLITE_OPEN_READWRITE;
480     if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
481     try{
482       let rc;
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);
487       });
488       let sql;
489       if( 0==rc && this.#db.initSql.length > 0){
490         rc = this.#runInitSql(pDb);
491       }
492       if( 0!=rc ){
493         sqlite3.SQLite3Error.toss(
494           rc,
495           "sqlite3 result code",rc+":",
496           (pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
497            : sqlite3.capi.sqlite3_errstr(rc))
498         );
499       }
500       return this.#db.list[this.#db.iCurrentDb] = pDb;
501     }catch(e){
502       sqlite3.capi.sqlite3_close_v2(pDb);
503       throw e;
504     }
505   }
507   addTestScript(ts){
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]));
514     }
515     if( !(ts instanceof TestScript) ){
516       Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
517     }
518     this.#aScripts.push(ts);
519     return this;
520   }
522   runTests(){
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){
529       this.reset();
530       ++this.metrics.nTestFile;
531       let threw = false;
532       const timeStart = (new Date()).getTime();
533       let msgTail = '';
534       try{
535         ts.run(this);
536       }catch(e){
537         if(e instanceof SQLTesterException){
538           threw = true;
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() ){
544             throw e;
545           }
546         }else{
547           throw e;
548         }
549       }finally{
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();
555         if( mod ){
556           this.out( "[",mod,"] " );
557         }
558         this.outln(ts.filename());
559       }
560     }
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
568                    : ""
569                )
570               );
571     return this;
572   }
574   #setupInitialDb(){
575     if( !this.#db.list[0] ){
576       Util.unlink(this.#db.initialDbName);
577       this.openDb(0, this.#db.initialDbName, true);
578     }else{
579       this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
580                         "triggered while it is opened.");
581     }
582   }
584   #escapeSqlValue(v){
585     if( !v ) return "{}";
586     if( !Rx.special.test(v) ){
587       return v  /* no escaping needed */;
588     }
589     if( !Rx.squiggly.test(v) ){
590       return "{"+v+"}";
591     }
592     const sb = ["\""];
593     const n = v.length;
594     for(let i = 0; i < n; ++i){
595       const ch = v.charAt(i);
596       switch(ch){
597         case '\\': sb.push("\\\\"); break;
598         case '"': sb.push("\\\""); break;
599         default:{
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');
603           else sb.push(ch);
604           break;
605         }
606       }
607     }
608     sb.append("\"");
609     return sb.join('');
610   }
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) ){
616       sb.push(msg);
617     }else{
618       sb.push('{', msg, '}');
619     }
620   }
622   #checkDbRc(pDb,rc){
623     sqlite3.oo1.DB.checkRc(pDb, rc);
624   }
626   execSql(pDb, throwOnError, appendMode, rowMode, sql){
627     if( !pDb && !this.#db.list[0] ){
628       this.#setupInitialDb();
629     }
630     if( !pDb ) pDb = this.#db.currentDb();
631     const wasm = sqlite3.wasm, capi = sqlite3.capi;
632     sql = (sql instanceof Uint8Array)
633       ? sql
634       : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
635     const self = this;
636     const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
637     let rc = 0;
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 */)
643       );
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
654         );
655         if( 0!==rc ){
656           if(throwOnError){
657             throw new DbException(self, pDb, rc);
658           }else if( sb ){
659             self.#appendDbErr(pDb, sb, rc);
660           }
661           break;
662         }
663         const pStmt = wasm.peekPtr(ppStmt);
664         pSql = wasm.peekPtr(pzTail);
665         sqlByteLen = pSqlEnd - pSql;
666         if(!pStmt) continue /* only whitespace or comments */;
667         if( sb ){
668           const nCol = capi.sqlite3_column_count(pStmt);
669           let colName, val;
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);
675                 switch(appendMode){
676                   case ResultBufferMode.ASIS: sb.push( colName ); break;
677                   case ResultBufferMode.ESCAPED:
678                     sb.push( self.#escapeSqlValue(colName) );
679                     break;
680                   default:
681                     self.toss("Unhandled ResultBufferMode.");
682                 }
683                 sb.push(' ');
684               }
685               val = capi.sqlite3_column_text(pStmt, i);
686               if( null===val ){
687                 sb.push( self.#nullView );
688                 continue;
689               }
690               switch(appendMode){
691                 case ResultBufferMode.ASIS: sb.push( val ); break;
692                 case ResultBufferMode.ESCAPED:
693                   sb.push( self.#escapeSqlValue(val) );
694                   break;
695               }
696             }/* column loop */
697           }/* row loop */
698           if( ResultRowMode.NEWLINE === rowMode ){
699             spacing = 0;
700             sb.push('\n');
701           }
702         }else{ // no output but possibly other side effects
703           while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
704         }
705         capi.sqlite3_finalize(pStmt);
706         if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
707         else if( rc!=0 ){
708           if( sb ){
709             self.#appendDbErr(db, sb, rc);
710           }
711           break;
712         }
713       }/* SQL script loop */;
714     })/*scopedAllocCall()*/;
715     return rc;
716   }
718 }/*SQLTester*/
720 class Command {
721   constructor(){
722   }
724   process(sqlTester,testScript,argv){
725     SQLTesterException.toss("process() must be overridden");
726   }
728   argcCheck(testScript,argv,min,max){
729     const argc = argv.length-1;
730     if(argc<min || (max>=0 && argc>max)){
731       if( min==max ){
732         testScript.toss(argv[0]," requires exactly ",min," argument(s)");
733       }else if(max>0){
734         testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
735       }else{
736         testScript.toss(argv[0]," requires at least ",min," arguments.");
737       }
738     }
739   }
742 class Cursor {
743   src;
744   sb = [];
745   pos = 0;
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.
750   putbackPos = 0;
751   //! Putback line number
752   putbackLineNo = 0;
753   //! Peeked-to pos, used by peekLine() and consumePeeked().
754   peekedPos = 0;
755   //! Peeked-to line number.
756   peekedLineNo = 0;
758   constructor(){
759   }
761   //! Restore parsing state to the start of the stream.
762   rewind(){
763     this.sb.length = this.pos = this.lineNo
764       = this.putbackPos = this.putbackLineNo
765       = this.peekedPos = this.peekedLineNo = 0;
766   }
769 class TestScript {
770   #cursor = new Cursor();
771   #moduleName = null;
772   #filename = null;
773   #testCaseName = null;
774   #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
776   constructor(...args){
777     let content, filename;
778     if( 2 == args.length ){
779       filename = args[0];
780       content = args[1];
781     }else if( 1 == args.length ){
782       if(args[0] instanceof Object){
783         const o = args[0];
784         filename = o.name;
785         content = o.content;
786       }else{
787         content = args[0];
788       }
789     }
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);
796       }else{
797         toss(Error, "Invalid content type for TestScript constructor.");
798       }
799     }
800     this.#filename = filename;
801     this.#cursor.src = content;
802   }
804   moduleName(){
805     return (0==arguments.length)
806       ? this.#moduleName : (this.#moduleName = arguments[0]);
807   }
809   testCaseName(){
810     return (0==arguments.length)
811       ? this.#testCaseName : (this.#testCaseName = arguments[0]);
812   }
813   filename(){
814     return (0==arguments.length)
815       ? this.#filename : (this.#filename = arguments[0]);
816   }
818   getOutputPrefix() {
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;
823   }
825   reset(){
826     this.#testCaseName = null;
827     this.#cursor.rewind();
828     return this;
829   }
831   toss(...args){
832     throw new TestScriptFailed(this,...args);
833   }
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); }
838   verbosity(...args){
839     const rc = this.#outer.verbosity(...args);
840     return args.length ? this : rc;
841   }
843   #checkRequiredProperties(tester, props){
844     if(true) return false;
845     let nOk = 0;
846     for(const rp of props){
847       this.verbose2("REQUIRED_PROPERTIES: ",rp);
848       switch(rp){
849         case "RECURSIVE_TRIGGERS":
850           tester.appendDbInitSql("pragma recursive_triggers=on;");
851           ++nOk;
852           break;
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;");
857           ++nOk;
858           break;
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;");
863           ++nOk;
864           break;
865         case "AUTOVACUUM":
866           tester.appendDbInitSql("pragma auto_vacuum=full;");
867           ++nOk;
868           break;
869         case "INCRVACUUM":
870           tester.appendDbInitSql("pragma auto_vacuum=incremental;");
871           ++nOk;
872         default:
873           break;
874       }
875     }
876     return props.length == nOk;
877   }
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);
884     }
885     let m = Rx.scriptModuleName.exec(line);
886     if( m ){
887       this.#moduleName = m[1];
888       return;
889     }
890     m = Rx.requiredProperties.exec(line);
891     if( m ){
892       const rp = m[1];
893       if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
894         throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
895       }
896     }
898     m = Rx.mixedModuleName.exec(line);
899     if( m ){
900       throw new IncompatibleDirective(this, m[1]+": "+m[3]);
901     }
902     if( line.indexOf("\n|")>=0 ){
903       throw new IncompatibleDirective(this, "newline-pipe combination.");
904     }
906   }
908   #getCommandArgv(line){
909     const m = Rx.command.exec(line);
910     return m ? m[1].trim().split(/\s+/) : null;
911   }
914   #isCommandLine(line, checkForImpl){
915     let m = Rx.command.exec(line);
916     if( m && checkForImpl ){
917       m = !!CommandDispatcher.getCommandByName(m[2]);
918     }
919     return !!m;
920   }
922   fetchCommandBody(tester){
923     const sb = [];
924     let line;
925     while( (null !== (line = this.peekLine())) ){
926       this.#checkForDirective(tester, line);
927       if( this.#isCommandLine(line, true) ) break;
928       sb.push(line,"\n");
929       this.consumePeeked();
930     }
931     line = sb.join('');
932     return !!line.trim() ? line : null;
933   }
935   run(tester){
936     this.reset();
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);
944       if( argv ){
945         this.#processCommand(tester, argv);
946         continue;
947       }
948       tester.appendInput(line,true);
949     }
950     return true;
951   }
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);
958     }
959     CommandDispatcher.dispatch(tester, this, argv);
960   }
962   getLine(){
963     const cur = this.#cursor;
964     if( cur.pos==cur.src.byteLength ){
965       return null/*EOF*/;
966     }
967     cur.putbackPos = cur.pos;
968     cur.putbackLineNo = cur.lineNo;
969     cur.sb.length = 0;
970     let b = 0, prevB = 0, i = cur.pos;
971     let doBreak = false;
972     let nChar = 0 /* number of bytes in the aChar char */;
973     const end = cur.src.byteLength;
974     for(; i < end && !doBreak; ++i){
975       b = cur.src[i];
976       switch( b ){
977         case 13/*CR*/: continue;
978         case 10/*NL*/:
979           ++cur.lineNo;
980           if(cur.sb.length>0) doBreak = true;
981           // Else it's an empty string
982           break;
983         default:{
984           /* Multi-byte chars need to be gathered up and appended at
985              one time so that we can get them as string objects. */
986           nChar = 1;
987           switch( b & 0xF0 ){
988             case 0xC0: nChar = 2; break;
989             case 0xE0: nChar = 3; break;
990             case 0xF0: nChar = 4; break;
991             default:
992               if( b > 127 ) this.toss("Invalid character (#"+b+").");
993               break;
994           }
995           if( 1==nChar ){
996             cur.sb.push(String.fromCharCode(b));
997           }else{
998             const aChar = [] /* multi-byte char buffer */;
999             for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
1000             cur.sb.push(
1001               Util.utf8Decode( new Uint8Array(aChar) )
1002             );
1003             i += nChar-1;
1004           }
1005           break;
1006         }
1007       }
1008     }
1009     cur.pos = i;
1010     const rv = cur.sb.join('');
1011     if( i==cur.src.byteLength && 0==rv.length ){
1012       return null /* EOF */;
1013     }
1014     return rv;
1015   }/*getLine()*/
1017   /**
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.
1021   */
1022   peekLine(){
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;
1028     try {
1029       return this.getLine();
1030     }finally{
1031       cur.peekedPos = cur.pos;
1032       cur.peekedLineNo = cur.lineNo;
1033       cur.pos = oldPos;
1034       cur.lineNo = oldLine;
1035       cur.putbackPos = oldPB;
1036       cur.putbackLineNo = oldPBL;
1037     }
1038   }
1041   /**
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().
1045   */
1046   consumePeeked(){
1047     const cur = this.#cursor;
1048     cur.pos = cur.peekedPos;
1049     cur.lineNo = cur.peekedLineNo;
1050   }
1052   /**
1053      Restores the cursor to the position it had before the previous
1054      call to getLine().
1055   */
1056   putbackLine(){
1057     const cur = this.#cursor;
1058     cur.pos = cur.putbackPos;
1059     cur.lineNo = cur.putbackLineNo;
1060   }
1062 }/*TestScript*/;
1064 //! --close command
1065 class CloseDbCommand extends Command {
1066   process(t, ts, argv){
1067     this.argcCheck(ts,argv,0,1);
1068     let id;
1069     if(argv.length>1){
1070       const arg = argv[1];
1071       if( "all" === arg ){
1072         t.closeAllDbs();
1073         return;
1074       }
1075       else{
1076         id = parseInt(arg);
1077       }
1078     }else{
1079       id = t.currentDbId();
1080     }
1081     t.closeDb(id);
1082   }
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]) );
1090   }
1093 //! --db command
1094 class DbCommand extends Command {
1095   process(t, ts, argv){
1096     this.argcCheck(ts,argv,1);
1097     t.currentDbId( parseInt(argv[1]) );
1098   }
1101 //! --glob command
1102 class GlobCommand extends Command {
1103   #negate = false;
1104   constructor(negate=false){
1105     super();
1106     this.#negate = negate;
1107   }
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);
1122     }
1123   }
1126 //! --notglob command
1127 class NotGlobCommand extends GlobCommand {
1128   constructor(){super(true);}
1131 //! --open command
1132 class OpenDbCommand extends Command {
1133   #createIfNeeded = false;
1134   constructor(createIfNeeded=false){
1135     super();
1136     this.#createIfNeeded = createIfNeeded;
1137   }
1138   process(t, ts, argv){
1139     this.argcCheck(ts,argv,1);
1140     t.openDb(argv[1], this.#createIfNeeded);
1141   }
1144 //! --new command
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){}
1154 //! --null command
1155 class NullCommand extends Command {
1156   process(st, ts, argv){
1157     this.argcCheck(ts,argv,1);
1158     st.nullValue( argv[1] );
1159   }
1162 //! --print command
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() );
1168     }else{
1169       st.outln( Util.argvToString(argv) );
1170     }
1171   }
1174 //! --result command
1175 class ResultCommand extends Command {
1176   #bufferMode;
1177   constructor(resultBufferMode = ResultBufferMode.ESCAPED){
1178     super();
1179     this.#bufferMode = resultBufferMode;
1180   }
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.");
1193     }
1194   }
1197 //! --json command
1198 class JsonCommand extends ResultCommand {
1199   constructor(){ super(ResultBufferMode.ASIS); }
1202 //! --run command
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);
1215     }
1216   }
1219 //! --tableresult command
1220 class TableResultCommand extends Command {
1221   #jsonMode;
1222   constructor(jsonMode=false){
1223     super();
1224     this.#jsonMode = jsonMode;
1225   }
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.");
1231     body = body.trim();
1232     if( !body.endsWith("\n--end") ){
1233       ts.toss(argv[0], " must be terminated with --end\\n");
1234     }else{
1235       body = body.substring(0, body.length-6);
1236     }
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"),".");
1241     }
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);
1251     }
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: <<",
1258                   res[i],">>");
1259         }
1260       }else if( 0!=Util.strglob(glob, res[i]) ){
1261         ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
1262       }
1263     }
1264   }
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();
1279   }
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]) );
1288   }
1291 class CommandDispatcher {
1292   static map = newObj();
1294   static getCommandByName(name){
1295     let rv = CommandDispatcher.map[name];
1296     if( rv ) return rv;
1297     switch(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;
1315     }
1316     if( rv ){
1317       CommandDispatcher.map[name] = rv;
1318     }
1319     return rv;
1320   }
1322   static dispatch(tester, testScript, argv){
1323     const cmd = CommandDispatcher.getCommandByName(argv[0]);
1324     if( !cmd ){
1325       toss(UnknownCommand,testScript,argv[0]);
1326     }
1327     cmd.process(tester, testScript, argv);
1328   }
1329 }/*CommandDispatcher*/
1331 const namespace = newObj({
1332   Command,
1333   DbException,
1334   IncompatibleDirective,
1335   Outer,
1336   SQLTester,
1337   SQLTesterException,
1338   TestScript,
1339   TestScriptFailed,
1340   UnknownCommand,
1341   Util,
1342   sqlite3
1345 export {namespace as default};