Snapshot of upstream SQLite 3.45.3
[sqlcipher.git] / ext / wasm / SQLTester / SQLTester.mjs
bloba72399aefcd5659d269286843feed839f0d58b33
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: /[{}]/
177 const Util = newObj({
178   toss,
180   unlink: function(fn){
181     return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn);
182   },
184   argvToString: (list)=>{
185     const m = [...list];
186     m.shift() /* strip command name */;
187     return m.join(" ")
188   },
190   utf8Decode: function(arrayBuffer, begin, end){
191     return __utf8Decoder.decode(
192       (arrayBuffer.buffer instanceof __SAB)
193         ? arrayBuffer.slice(begin, end)
194         : arrayBuffer.subarray(begin, end)
195     );
196   },
198   utf8Encode: (str)=>__utf8Encoder.encode(str),
200   strglob: sqlite3.wasm.xWrap('sqlite3_wasm_SQLTester_strglob','int',
201                               ['string','string'])
202 })/*Util*/;
204 class Outer {
205   #lnBuf = [];
206   #verbosity = 0;
207   #logger = console.log.bind(console);
209   constructor(func){
210     if(func) this.setFunc(func);
211   }
213   logger(...args){
214     if(args.length){
215       this.#logger = args[0];
216       return this;
217     }
218     return this.#logger;
219   }
221   out(...args){
222     if( this.getOutputPrefix && !this.#lnBuf.length ){
223       this.#lnBuf.push(this.getOutputPrefix());
224     }
225     this.#lnBuf.push(...args);
226     return this;
227   }
229   #outlnImpl(vLevel, ...args){
230     if( this.getOutputPrefix && !this.#lnBuf.length ){
231       this.#lnBuf.push(this.getOutputPrefix());
232     }
233     this.#lnBuf.push(...args,'\n');
234     const msg = this.#lnBuf.join('');
235     this.#lnBuf.length = 0;
236     this.#logger(msg);
237     return this;
238   }
240   outln(...args){
241     return this.#outlnImpl(0,...args);
242   }
244   outputPrefix(){
245     if( 0==arguments.length ){
246       return (this.getOutputPrefix
247               ? (this.getOutputPrefix() ?? '') : '');
248     }else{
249       this.getOutputPrefix = arguments[0];
250       return this;
251     }
252   }
254   static #verboseLabel = ["🔈",/*"🔉",*/"🔊","📢"];
255   verboseN(lvl, args){
256     if( this.#verbosity>=lvl ){
257       this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
258     }
259   }
260   verbose1(...args){ return this.verboseN(1,args); }
261   verbose2(...args){ return this.verboseN(2,args); }
262   verbose3(...args){ return this.verboseN(3,args); }
264   verbosity(){
265     const rc = this.#verbosity;
266     if(arguments.length) this.#verbosity = +arguments[0];
267     return rc;
268   }
270 }/*Outer*/
272 class SQLTester {
274   //! Console output utility.
275   #outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
276   //! List of input scripts.
277   #aScripts = [];
278   //! Test input buffer.
279   #inputBuffer = [];
280   //! Test result buffer.
281   #resultBuffer = [];
282   //! Output representation of SQL NULL.
283   #nullView;
284   metrics = newObj({
285     //! Total tests run
286     nTotalTest: 0,
287     //! Total test script files run
288     nTestFile: 0,
289     //! Test-case count for to the current TestScript
290     nTest: 0,
291     //! Names of scripts which were aborted.
292     failedScripts: []
293   });
294   #emitColNames = false;
295   //! True to keep going regardless of how a test fails.
296   #keepGoing = false;
297   #db = newObj({
298     //! The list of available db handles.
299     list: new Array(7),
300     //! Index into this.list of the current db.
301     iCurrentDb: 0,
302     //! Name of the default db, re-created for each script.
303     initialDbName: "test.db",
304     //! Buffer for REQUIRED_PROPERTIES pragmas.
305     initSql: ['select 1;'],
306     //! (sqlite3*) to the current db.
307     currentDb: function(){
308       return this.list[this.iCurrentDb];
309     }
310   });
312   constructor(){
313     this.reset();
314   }
316   outln(...args){ return this.#outer.outln(...args); }
317   out(...args){ return this.#outer.out(...args); }
318   outer(...args){
319     if(args.length){
320       this.#outer = args[0];
321       return this;
322     }
323     return this.#outer;
324   }
325   verbose1(...args){ return this.#outer.verboseN(1,args); }
326   verbose2(...args){ return this.#outer.verboseN(2,args); }
327   verbose3(...args){ return this.#outer.verboseN(3,args); }
328   verbosity(...args){
329     const rc = this.#outer.verbosity(...args);
330     return args.length ? this : rc;
331   }
332   setLogger(func){
333     this.#outer.logger(func);
334     return this;
335   }
337   incrementTestCounter(){
338     ++this.metrics.nTotalTest;
339     ++this.metrics.nTest;
340   }
342   reset(){
343     this.clearInputBuffer();
344     this.clearResultBuffer();
345     this.#clearBuffer(this.#db.initSql);
346     this.closeAllDbs();
347     this.metrics.nTest = 0;
348     this.#nullView = "nil";
349     this.emitColNames = false;
350     this.#db.iCurrentDb = 0;
351     //this.#db.initSql.push("SELECT 1;");
352   }
354   appendInput(line, addNL){
355     this.#inputBuffer.push(line);
356     if( addNL ) this.#inputBuffer.push('\n');
357   }
358   appendResult(line, addNL){
359     this.#resultBuffer.push(line);
360     if( addNL ) this.#resultBuffer.push('\n');
361   }
362   appendDbInitSql(sql){
363     this.#db.initSql.push(sql);
364     if( this.currentDb() ){
365       this.execSql(null, true, ResultBufferMode.NONE, null, sql);
366     }
367   }
369   #runInitSql(pDb){
370     let rc = 0;
371     for(const sql of this.#db.initSql){
372       this.#outer.verbose2("RUNNING DB INIT CODE: ",sql);
373       rc = this.execSql(pDb, false, ResultBufferMode.NONE, null, sql);
374       if( rc ) break;
375     }
376     return rc;
377   }
379 #clearBuffer(buffer){
380     buffer.length = 0;
381     return buffer;
382   }
384   clearInputBuffer(){ return this.#clearBuffer(this.#inputBuffer); }
385   clearResultBuffer(){return this.#clearBuffer(this.#resultBuffer); }
387   getInputText(){ return this.#inputBuffer.join(''); }
388   getResultText(){ return this.#resultBuffer.join(''); }
390   #takeBuffer(buffer){
391     const s = buffer.join('');
392     buffer.length = 0;
393     return s;
394   }
396   takeInputBuffer(){
397     return this.#takeBuffer(this.#inputBuffer);
398   }
399   takeResultBuffer(){
400     return this.#takeBuffer(this.#resultBuffer);
401   }
403   nullValue(){
404     return (0==arguments.length)
405       ? this.#nullView
406       : (this.#nullView = ''+arguments[0]);
407   }
409   outputColumnNames(){
410     return (0==arguments.length)
411       ? this.#emitColNames
412       : (this.#emitColNames = !!arguments[0]);
413   }
415   currentDbId(){
416     return (0==arguments.length)
417       ? this.#db.iCurrentDb
418       : (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
419   }
421   #affirmDbId(id){
422     if(id<0 || id>=this.#db.list.length){
423       toss(SQLTesterException, "Database index ",id," is out of range.");
424     }
425     return this;
426   }
428   currentDb(...args){
429     if( 0!=args.length ){
430       this.#affirmDbId(id).#db.iCurrentDb = id;
431     }
432     return this.#db.currentDb();
433   }
435   getDbById(id){
436     return this.#affirmDbId(id).#db.list[id];
437   }
439   getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
442   closeDb(id) {
443     if( 0==arguments.length ){
444       id = this.#db.iCurrentDb;
445     }
446     const pDb = this.#affirmDbId(id).#db.list[id];
447     if( pDb ){
448       sqlite3.capi.sqlite3_close_v2(pDb);
449       this.#db.list[id] = null;
450     }
451   }
453   closeAllDbs(){
454     for(let i = 0; i<this.#db.list.length; ++i){
455       if(this.#db.list[i]){
456         sqlite3.capi.sqlite3_close_v2(this.#db.list[i]);
457         this.#db.list[i] = null;
458       }
459     }
460     this.#db.iCurrentDb = 0;
461   }
463   openDb(name, createIfNeeded){
464     if( 3===arguments.length ){
465       const slot = arguments[0];
466       this.#affirmDbId(slot).#db.iCurrentDb = slot;
467       name = arguments[1];
468       createIfNeeded = arguments[2];
469     }
470     this.closeDb();
471     const capi = sqlite3.capi, wasm = sqlite3.wasm;
472     let pDb = 0;
473     let flags = capi.SQLITE_OPEN_READWRITE;
474     if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
475     try{
476       let rc;
477       wasm.pstack.call(function(){
478         let ppOut = wasm.pstack.allocPtr();
479         rc = sqlite3.capi.sqlite3_open_v2(name, ppOut, flags, null);
480         pDb = wasm.peekPtr(ppOut);
481       });
482       let sql;
483       if( 0==rc && this.#db.initSql.length > 0){
484         rc = this.#runInitSql(pDb);
485       }
486       if( 0!=rc ){
487         sqlite3.SQLite3Error.toss(
488           rc,
489           "sqlite3 result code",rc+":",
490           (pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
491            : sqlite3.capi.sqlite3_errstr(rc))
492         );
493       }
494       return this.#db.list[this.#db.iCurrentDb] = pDb;
495     }catch(e){
496       sqlite3.capi.sqlite3_close_v2(pDb);
497       throw e;
498     }
499   }
501   addTestScript(ts){
502     if( 2===arguments.length ){
503       ts = new TestScript(arguments[0], arguments[1]);
504     }else if(ts instanceof Uint8Array){
505       ts = new TestScript('<unnamed>', ts);
506     }else if('string' === typeof arguments[1]){
507       ts = new TestScript('<unnamed>', Util.utf8Encode(arguments[1]));
508     }
509     if( !(ts instanceof TestScript) ){
510       Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
511     }
512     this.#aScripts.push(ts);
513     return this;
514   }
516   runTests(){
517     const tStart = (new Date()).getTime();
518     let isVerbose = this.verbosity();
519     this.metrics.failedScripts.length = 0;
520     this.metrics.nTotalTest = 0;
521     this.metrics.nTestFile = 0;
522     for(const ts of this.#aScripts){
523       this.reset();
524       ++this.metrics.nTestFile;
525       let threw = false;
526       const timeStart = (new Date()).getTime();
527       let msgTail = '';
528       try{
529         ts.run(this);
530       }catch(e){
531         if(e instanceof SQLTesterException){
532           threw = true;
533           this.outln("🔥EXCEPTION: ",e);
534           this.metrics.failedScripts.push({script: ts.filename(), message:e.toString()});
535           if( this.#keepGoing ){
536             this.outln("Continuing anyway because of the keep-going option.");
537           }else if( e.isFatal() ){
538             throw e;
539           }
540         }else{
541           throw e;
542         }
543       }finally{
544         const timeEnd = (new Date()).getTime();
545         this.out("🏁", (threw ? "❌" : "✅"), " ",
546                  this.metrics.nTest, " test(s) in ",
547                  (timeEnd-timeStart),"ms. ");
548         const mod = ts.moduleName();
549         if( mod ){
550           this.out( "[",mod,"] " );
551         }
552         this.outln(ts.filename());
553       }
554     }
555     const tEnd = (new Date()).getTime();
556     Util.unlink(this.#db.initialDbName);
557     this.outln("Took ",(tEnd-tStart),"ms. Test count = ",
558                this.metrics.nTotalTest,", script count = ",
559                this.#aScripts.length,(
560                  this.metrics.failedScripts.length
561                    ? ", failed scripts = "+this.metrics.failedScripts.length
562                    : ""
563                )
564               );
565     return this;
566   }
568   #setupInitialDb(){
569     if( !this.#db.list[0] ){
570       Util.unlink(this.#db.initialDbName);
571       this.openDb(0, this.#db.initialDbName, true);
572     }else{
573       this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
574                         "triggered while it is opened.");
575     }
576   }
578   #escapeSqlValue(v){
579     if( !v ) return "{}";
580     if( !Rx.special.test(v) ){
581       return v  /* no escaping needed */;
582     }
583     if( !Rx.squiggly.test(v) ){
584       return "{"+v+"}";
585     }
586     const sb = ["\""];
587     const n = v.length;
588     for(let i = 0; i < n; ++i){
589       const ch = v.charAt(i);
590       switch(ch){
591         case '\\': sb.push("\\\\"); break;
592         case '"': sb.push("\\\""); break;
593         default:{
594           //verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
595           const ccode = ch.charCodeAt(i);
596           if( ccode < 32 ) sb.push('\\',ccode.toString(8),'o');
597           else sb.push(ch);
598           break;
599         }
600       }
601     }
602     sb.append("\"");
603     return sb.join('');
604   }
606   #appendDbErr(pDb, sb, rc){
607     sb.push(sqlite3.capi.sqlite3_js_rc_str(rc), ' ');
608     const msg = this.#escapeSqlValue(sqlite3.capi.sqlite3_errmsg(pDb));
609     if( '{' === msg.charAt(0) ){
610       sb.push(msg);
611     }else{
612       sb.push('{', msg, '}');
613     }
614   }
616   #checkDbRc(pDb,rc){
617     sqlite3.oo1.DB.checkRc(pDb, rc);
618   }
620   execSql(pDb, throwOnError, appendMode, rowMode, sql){
621     if( !pDb && !this.#db.list[0] ){
622       this.#setupInitialDb();
623     }
624     if( !pDb ) pDb = this.#db.currentDb();
625     const wasm = sqlite3.wasm, capi = sqlite3.capi;
626     sql = (sql instanceof Uint8Array)
627       ? sql
628       : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
629     const self = this;
630     const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
631     let rc = 0;
632     wasm.scopedAllocCall(function(){
633       let sqlByteLen = sql.byteLength;
634       const ppStmt = wasm.scopedAlloc(
635         /* output (sqlite3_stmt**) arg and pzTail */
636         (2 * wasm.ptrSizeof) + (sqlByteLen + 1/* SQL + NUL */)
637       );
638       const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
639       let pSql = pzTail + wasm.ptrSizeof;
640       const pSqlEnd = pSql + sqlByteLen;
641       wasm.heap8().set(sql, pSql);
642       wasm.poke8(pSql + sqlByteLen, 0/*NUL terminator*/);
643       let pos = 0, n = 1, spacing = 0;
644       while( pSql && wasm.peek8(pSql) ){
645         wasm.pokePtr([ppStmt, pzTail], 0);
646         rc = capi.sqlite3_prepare_v3(
647           pDb, pSql, sqlByteLen, 0, ppStmt, pzTail
648         );
649         if( 0!==rc ){
650           if(throwOnError){
651             throw new DbException(self, pDb, rc);
652           }else if( sb ){
653             self.#appendDbErr(pDb, sb, rc);
654           }
655           break;
656         }
657         const pStmt = wasm.peekPtr(ppStmt);
658         pSql = wasm.peekPtr(pzTail);
659         sqlByteLen = pSqlEnd - pSql;
660         if(!pStmt) continue /* only whitespace or comments */;
661         if( sb ){
662           const nCol = capi.sqlite3_column_count(pStmt);
663           let colName, val;
664           while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {
665             for( let i=0; i < nCol; ++i ){
666               if( spacing++ > 0 ) sb.push(' ');
667               if( self.#emitColNames ){
668                 colName = capi.sqlite3_column_name(pStmt, i);
669                 switch(appendMode){
670                   case ResultBufferMode.ASIS: sb.push( colName ); break;
671                   case ResultBufferMode.ESCAPED:
672                     sb.push( self.#escapeSqlValue(colName) );
673                     break;
674                   default:
675                     self.toss("Unhandled ResultBufferMode.");
676                 }
677                 sb.push(' ');
678               }
679               val = capi.sqlite3_column_text(pStmt, i);
680               if( null===val ){
681                 sb.push( self.#nullView );
682                 continue;
683               }
684               switch(appendMode){
685                 case ResultBufferMode.ASIS: sb.push( val ); break;
686                 case ResultBufferMode.ESCAPED:
687                   sb.push( self.#escapeSqlValue(val) );
688                   break;
689               }
690             }/* column loop */
691           }/* row loop */
692           if( ResultRowMode.NEWLINE === rowMode ){
693             spacing = 0;
694             sb.push('\n');
695           }
696         }else{ // no output but possibly other side effects
697           while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
698         }
699         capi.sqlite3_finalize(pStmt);
700         if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
701         else if( rc!=0 ){
702           if( sb ){
703             self.#appendDbErr(db, sb, rc);
704           }
705           break;
706         }
707       }/* SQL script loop */;
708     })/*scopedAllocCall()*/;
709     return rc;
710   }
712 }/*SQLTester*/
714 class Command {
715   constructor(){
716   }
718   process(sqlTester,testScript,argv){
719     SQLTesterException.toss("process() must be overridden");
720   }
722   argcCheck(testScript,argv,min,max){
723     const argc = argv.length-1;
724     if(argc<min || (max>=0 && argc>max)){
725       if( min==max ){
726         testScript.toss(argv[0]," requires exactly ",min," argument(s)");
727       }else if(max>0){
728         testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
729       }else{
730         testScript.toss(argv[0]," requires at least ",min," arguments.");
731       }
732     }
733   }
736 class Cursor {
737   src;
738   sb = [];
739   pos = 0;
740   //! Current line number. Starts at 0 for internal reasons and will
741   // line up with 1-based reality once parsing starts.
742   lineNo = 0 /* yes, zero */;
743   //! Putback value for this.pos.
744   putbackPos = 0;
745   //! Putback line number
746   putbackLineNo = 0;
747   //! Peeked-to pos, used by peekLine() and consumePeeked().
748   peekedPos = 0;
749   //! Peeked-to line number.
750   peekedLineNo = 0;
752   constructor(){
753   }
755   //! Restore parsing state to the start of the stream.
756   rewind(){
757     this.sb.length = this.pos = this.lineNo
758       = this.putbackPos = this.putbackLineNo
759       = this.peekedPos = this.peekedLineNo = 0;
760   }
763 class TestScript {
764   #cursor = new Cursor();
765   #moduleName = null;
766   #filename = null;
767   #testCaseName = null;
768   #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
770   constructor(...args){
771     let content, filename;
772     if( 2 == args.length ){
773       filename = args[0];
774       content = args[1];
775     }else if( 1 == args.length ){
776       if(args[0] instanceof Object){
777         const o = args[0];
778         filename = o.name;
779         content = o.content;
780       }else{
781         content = args[0];
782       }
783     }
784     if(!(content instanceof Uint8Array)){
785       if('string' === typeof content){
786         content = Util.utf8Encode(content);
787       }else if((content instanceof ArrayBuffer)
788                ||(content instanceof Array)){
789         content = new Uint8Array(content);
790       }else{
791         toss(Error, "Invalid content type for TestScript constructor.");
792       }
793     }
794     this.#filename = filename;
795     this.#cursor.src = content;
796   }
798   moduleName(){
799     return (0==arguments.length)
800       ? this.#moduleName : (this.#moduleName = arguments[0]);
801   }
803   testCaseName(){
804     return (0==arguments.length)
805       ? this.#testCaseName : (this.#testCaseName = arguments[0]);
806   }
807   filename(){
808     return (0==arguments.length)
809       ? this.#filename : (this.#filename = arguments[0]);
810   }
812   getOutputPrefix() {
813     let rc =  "["+(this.#moduleName || '<unnamed>')+"]";
814     if( this.#testCaseName ) rc += "["+this.#testCaseName+"]";
815     if( this.#filename ) rc += '['+this.#filename+']';
816     return rc + " line "+ this.#cursor.lineNo;
817   }
819   reset(){
820     this.#testCaseName = null;
821     this.#cursor.rewind();
822     return this;
823   }
825   toss(...args){
826     throw new TestScriptFailed(this,...args);
827   }
829   verbose1(...args){ return this.#outer.verboseN(1,args); }
830   verbose2(...args){ return this.#outer.verboseN(2,args); }
831   verbose3(...args){ return this.#outer.verboseN(3,args); }
832   verbosity(...args){
833     const rc = this.#outer.verbosity(...args);
834     return args.length ? this : rc;
835   }
837   #checkRequiredProperties(tester, props){
838     if(true) return false;
839     let nOk = 0;
840     for(const rp of props){
841       this.verbose2("REQUIRED_PROPERTIES: ",rp);
842       switch(rp){
843         case "RECURSIVE_TRIGGERS":
844           tester.appendDbInitSql("pragma recursive_triggers=on;");
845           ++nOk;
846           break;
847         case "TEMPSTORE_FILE":
848           /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
849              which we just happen to know is the case */
850           tester.appendDbInitSql("pragma temp_store=1;");
851           ++nOk;
852           break;
853         case "TEMPSTORE_MEM":
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=0;");
857           ++nOk;
858           break;
859         case "AUTOVACUUM":
860           tester.appendDbInitSql("pragma auto_vacuum=full;");
861           ++nOk;
862           break;
863         case "INCRVACUUM":
864           tester.appendDbInitSql("pragma auto_vacuum=incremental;");
865           ++nOk;
866         default:
867           break;
868       }
869     }
870     return props.length == nOk;
871   }
873   #checkForDirective(tester,line){
874     if(line.startsWith("#")){
875       throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
876     }else if(line.startsWith("---")){
877       throw new IncompatibleDirective(this, "triple-dash: ",line);
878     }
879     let m = Rx.scriptModuleName.exec(line);
880     if( m ){
881       this.#moduleName = m[1];
882       return;
883     }
884     m = Rx.requiredProperties.exec(line);
885     if( m ){
886       const rp = m[1];
887       if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
888         throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
889       }
890     }
892     m = Rx.mixedModuleName.exec(line);
893     if( m ){
894       throw new IncompatibleDirective(this, m[1]+": "+m[3]);
895     }
896     if( line.indexOf("\n|")>=0 ){
897       throw new IncompatibleDirective(this, "newline-pipe combination.");
898     }
900   }
902   #getCommandArgv(line){
903     const m = Rx.command.exec(line);
904     return m ? m[1].trim().split(/\s+/) : null;
905   }
908   #isCommandLine(line, checkForImpl){
909     let m = Rx.command.exec(line);
910     if( m && checkForImpl ){
911       m = !!CommandDispatcher.getCommandByName(m[2]);
912     }
913     return !!m;
914   }
916   fetchCommandBody(tester){
917     const sb = [];
918     let line;
919     while( (null !== (line = this.peekLine())) ){
920       this.#checkForDirective(tester, line);
921       if( this.#isCommandLine(line, true) ) break;
922       sb.push(line,"\n");
923       this.consumePeeked();
924     }
925     line = sb.join('');
926     return !!line.trim() ? line : null;
927   }
929   run(tester){
930     this.reset();
931     this.#outer.verbosity( tester.verbosity() );
932     this.#outer.logger( tester.outer().logger() );
933     let line, directive, argv = [];
934     while( null != (line = this.getLine()) ){
935       this.verbose3("run() input line: ",line);
936       this.#checkForDirective(tester, line);
937       argv = this.#getCommandArgv(line);
938       if( argv ){
939         this.#processCommand(tester, argv);
940         continue;
941       }
942       tester.appendInput(line,true);
943     }
944     return true;
945   }
947   #processCommand(tester, argv){
948     this.verbose2("processCommand(): ",argv[0], " ", Util.argvToString(argv));
949     if(this.#outer.verbosity()>1){
950       const input = tester.getInputText();
951       this.verbose3("processCommand() input buffer = ",input);
952     }
953     CommandDispatcher.dispatch(tester, this, argv);
954   }
956   getLine(){
957     const cur = this.#cursor;
958     if( cur.pos==cur.src.byteLength ){
959       return null/*EOF*/;
960     }
961     cur.putbackPos = cur.pos;
962     cur.putbackLineNo = cur.lineNo;
963     cur.sb.length = 0;
964     let b = 0, prevB = 0, i = cur.pos;
965     let doBreak = false;
966     let nChar = 0 /* number of bytes in the aChar char */;
967     const end = cur.src.byteLength;
968     for(; i < end && !doBreak; ++i){
969       b = cur.src[i];
970       switch( b ){
971         case 13/*CR*/: continue;
972         case 10/*NL*/:
973           ++cur.lineNo;
974           if(cur.sb.length>0) doBreak = true;
975           // Else it's an empty string
976           break;
977         default:{
978           /* Multi-byte chars need to be gathered up and appended at
979              one time so that we can get them as string objects. */
980           nChar = 1;
981           switch( b & 0xF0 ){
982             case 0xC0: nChar = 2; break;
983             case 0xE0: nChar = 3; break;
984             case 0xF0: nChar = 4; break;
985             default:
986               if( b > 127 ) this.toss("Invalid character (#"+b+").");
987               break;
988           }
989           if( 1==nChar ){
990             cur.sb.push(String.fromCharCode(b));
991           }else{
992             const aChar = [] /* multi-byte char buffer */;
993             for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
994             cur.sb.push(
995               Util.utf8Decode( new Uint8Array(aChar) )
996             );
997             i += nChar-1;
998           }
999           break;
1000         }
1001       }
1002     }
1003     cur.pos = i;
1004     const rv = cur.sb.join('');
1005     if( i==cur.src.byteLength && 0==rv.length ){
1006       return null /* EOF */;
1007     }
1008     return rv;
1009   }/*getLine()*/
1011   /**
1012      Fetches the next line then resets the cursor to its pre-call
1013      state. consumePeeked() can be used to consume this peeked line
1014      without having to re-parse it.
1015   */
1016   peekLine(){
1017     const cur = this.#cursor;
1018     const oldPos = cur.pos;
1019     const oldPB = cur.putbackPos;
1020     const oldPBL = cur.putbackLineNo;
1021     const oldLine = cur.lineNo;
1022     try {
1023       return this.getLine();
1024     }finally{
1025       cur.peekedPos = cur.pos;
1026       cur.peekedLineNo = cur.lineNo;
1027       cur.pos = oldPos;
1028       cur.lineNo = oldLine;
1029       cur.putbackPos = oldPB;
1030       cur.putbackLineNo = oldPBL;
1031     }
1032   }
1035   /**
1036      Only valid after calling peekLine() and before calling getLine().
1037      This places the cursor to the position it would have been at had
1038      the peekLine() had been fetched with getLine().
1039   */
1040   consumePeeked(){
1041     const cur = this.#cursor;
1042     cur.pos = cur.peekedPos;
1043     cur.lineNo = cur.peekedLineNo;
1044   }
1046   /**
1047      Restores the cursor to the position it had before the previous
1048      call to getLine().
1049   */
1050   putbackLine(){
1051     const cur = this.#cursor;
1052     cur.pos = cur.putbackPos;
1053     cur.lineNo = cur.putbackLineNo;
1054   }
1056 }/*TestScript*/;
1058 //! --close command
1059 class CloseDbCommand extends Command {
1060   process(t, ts, argv){
1061     this.argcCheck(ts,argv,0,1);
1062     let id;
1063     if(argv.length>1){
1064       const arg = argv[1];
1065       if( "all" === arg ){
1066         t.closeAllDbs();
1067         return;
1068       }
1069       else{
1070         id = parseInt(arg);
1071       }
1072     }else{
1073       id = t.currentDbId();
1074     }
1075     t.closeDb(id);
1076   }
1079 //! --column-names command
1080 class ColumnNamesCommand extends Command {
1081   process( st, ts, argv ){
1082     this.argcCheck(ts,argv,1);
1083     st.outputColumnNames( !!parseInt(argv[1]) );
1084   }
1087 //! --db command
1088 class DbCommand extends Command {
1089   process(t, ts, argv){
1090     this.argcCheck(ts,argv,1);
1091     t.currentDbId( parseInt(argv[1]) );
1092   }
1095 //! --glob command
1096 class GlobCommand extends Command {
1097   #negate = false;
1098   constructor(negate=false){
1099     super();
1100     this.#negate = negate;
1101   }
1103   process(t, ts, argv){
1104     this.argcCheck(ts,argv,1,-1);
1105     t.incrementTestCounter();
1106     const sql = t.takeInputBuffer();
1107     let rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
1108                        ResultRowMode.ONELINE, sql);
1109     const result = t.getResultText();
1110     const sArgs = Util.argvToString(argv);
1111     //t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
1112     const glob = Util.argvToString(argv);
1113     rc = Util.strglob(glob, result);
1114     if( (this.#negate && 0===rc) || (!this.#negate && 0!==rc) ){
1115       ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
1116     }
1117   }
1120 //! --notglob command
1121 class NotGlobCommand extends GlobCommand {
1122   constructor(){super(true);}
1125 //! --open command
1126 class OpenDbCommand extends Command {
1127   #createIfNeeded = false;
1128   constructor(createIfNeeded=false){
1129     super();
1130     this.#createIfNeeded = createIfNeeded;
1131   }
1132   process(t, ts, argv){
1133     this.argcCheck(ts,argv,1);
1134     t.openDb(argv[1], this.#createIfNeeded);
1135   }
1138 //! --new command
1139 class NewDbCommand extends OpenDbCommand {
1140   constructor(){ super(true); }
1143 //! Placeholder dummy/no-op commands
1144 class NoopCommand extends Command {
1145   process(t, ts, argv){}
1148 //! --null command
1149 class NullCommand extends Command {
1150   process(st, ts, argv){
1151     this.argcCheck(ts,argv,1);
1152     st.nullValue( argv[1] );
1153   }
1156 //! --print command
1157 class PrintCommand extends Command {
1158   process(st, ts, argv){
1159     st.out(ts.getOutputPrefix(),': ');
1160     if( 1==argv.length ){
1161       st.out( st.getInputText() );
1162     }else{
1163       st.outln( Util.argvToString(argv) );
1164     }
1165   }
1168 //! --result command
1169 class ResultCommand extends Command {
1170   #bufferMode;
1171   constructor(resultBufferMode = ResultBufferMode.ESCAPED){
1172     super();
1173     this.#bufferMode = resultBufferMode;
1174   }
1175   process(t, ts, argv){
1176     this.argcCheck(ts,argv,0,-1);
1177     t.incrementTestCounter();
1178     const sql = t.takeInputBuffer();
1179     //ts.verbose2(argv[0]," SQL =\n",sql);
1180     t.execSql(null, false, this.#bufferMode, ResultRowMode.ONELINE, sql);
1181     const result = t.getResultText().trim();
1182     const sArgs = argv.length>1 ? Util.argvToString(argv) : "";
1183     if( result !== sArgs ){
1184       t.outln(argv[0]," FAILED comparison. Result buffer:\n",
1185               result,"\nExpected result:\n",sArgs);
1186       ts.toss(argv[0]+" comparison failed.");
1187     }
1188   }
1191 //! --json command
1192 class JsonCommand extends ResultCommand {
1193   constructor(){ super(ResultBufferMode.ASIS); }
1196 //! --run command
1197 class RunCommand extends Command {
1198   process(t, ts, argv){
1199     this.argcCheck(ts,argv,0,1);
1200     const pDb = (1==argv.length)
1201       ? t.currentDb() : t.getDbById( parseInt(argv[1]) );
1202     const sql = t.takeInputBuffer();
1203     const rc = t.execSql(pDb, false, ResultBufferMode.NONE,
1204                        ResultRowMode.ONELINE, sql);
1205     if( 0!==rc && t.verbosity()>0 ){
1206       const msg = sqlite3.capi.sqlite3_errmsg(pDb);
1207       ts.verbose2(argv[0]," non-fatal command error #",rc,": ",
1208                   msg,"\nfor SQL:\n",sql);
1209     }
1210   }
1213 //! --tableresult command
1214 class TableResultCommand extends Command {
1215   #jsonMode;
1216   constructor(jsonMode=false){
1217     super();
1218     this.#jsonMode = jsonMode;
1219   }
1220   process(t, ts, argv){
1221     this.argcCheck(ts,argv,0);
1222     t.incrementTestCounter();
1223     let body = ts.fetchCommandBody(t);
1224     if( null===body ) ts.toss("Missing ",argv[0]," body.");
1225     body = body.trim();
1226     if( !body.endsWith("\n--end") ){
1227       ts.toss(argv[0], " must be terminated with --end\\n");
1228     }else{
1229       body = body.substring(0, body.length-6);
1230     }
1231     const globs = body.split(/\s*\n\s*/);
1232     if( globs.length < 1 ){
1233       ts.toss(argv[0], " requires 1 or more ",
1234               (this.#jsonMode ? "json snippets" : "globs"),".");
1235     }
1236     const sql = t.takeInputBuffer();
1237     t.execSql(null, true,
1238               this.#jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
1239               ResultRowMode.NEWLINE, sql);
1240     const rbuf = t.getResultText().trim();
1241     const res = rbuf.split(/\r?\n/);
1242     if( res.length !== globs.length ){
1243       ts.toss(argv[0], " failure: input has ", res.length,
1244               " row(s) but expecting ",globs.length);
1245     }
1246     for(let i = 0; i < res.length; ++i){
1247       const glob = globs[i].replaceAll(/\s+/g," ").trim();
1248       //ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
1249       if( this.#jsonMode ){
1250         if( glob!==res[i] ){
1251           ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
1252                   res[i],">>");
1253         }
1254       }else if( 0!=Util.strglob(glob, res[i]) ){
1255         ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
1256       }
1257     }
1258   }
1261 //! --json-block command
1262 class JsonBlockCommand extends TableResultCommand {
1263   constructor(){ super(true); }
1266 //! --testcase command
1267 class TestCaseCommand extends Command {
1268   process(tester, script, argv){
1269     this.argcCheck(script, argv,1);
1270     script.testCaseName(argv[1]);
1271     tester.clearResultBuffer();
1272     tester.clearInputBuffer();
1273   }
1277 //! --verbosity command
1278 class VerbosityCommand extends Command {
1279   process(t, ts, argv){
1280     this.argcCheck(ts,argv,1);
1281     ts.verbosity( parseInt(argv[1]) );
1282   }
1285 class CommandDispatcher {
1286   static map = newObj();
1288   static getCommandByName(name){
1289     let rv = CommandDispatcher.map[name];
1290     if( rv ) return rv;
1291     switch(name){
1292       case "close":        rv = new CloseDbCommand(); break;
1293       case "column-names": rv = new ColumnNamesCommand(); break;
1294       case "db":           rv = new DbCommand(); break;
1295       case "glob":         rv = new GlobCommand(); break;
1296       case "json":         rv = new JsonCommand(); break;
1297       case "json-block":   rv = new JsonBlockCommand(); break;
1298       case "new":          rv = new NewDbCommand(); break;
1299       case "notglob":      rv = new NotGlobCommand(); break;
1300       case "null":         rv = new NullCommand(); break;
1301       case "oom":          rv = new NoopCommand(); break;
1302       case "open":         rv = new OpenDbCommand(); break;
1303       case "print":        rv = new PrintCommand(); break;
1304       case "result":       rv = new ResultCommand(); break;
1305       case "run":          rv = new RunCommand(); break;
1306       case "tableresult":  rv = new TableResultCommand(); break;
1307       case "testcase":     rv = new TestCaseCommand(); break;
1308       case "verbosity":    rv = new VerbosityCommand(); break;
1309     }
1310     if( rv ){
1311       CommandDispatcher.map[name] = rv;
1312     }
1313     return rv;
1314   }
1316   static dispatch(tester, testScript, argv){
1317     const cmd = CommandDispatcher.getCommandByName(argv[0]);
1318     if( !cmd ){
1319       toss(UnknownCommand,testScript,argv[0]);
1320     }
1321     cmd.process(tester, testScript, argv);
1322   }
1323 }/*CommandDispatcher*/
1325 const namespace = newObj({
1326   Command,
1327   DbException,
1328   IncompatibleDirective,
1329   Outer,
1330   SQLTester,
1331   SQLTesterException,
1332   TestScript,
1333   TestScriptFailed,
1334   UnknownCommand,
1335   Util,
1336   sqlite3
1339 export {namespace as default};