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]/,
177 const Util = newObj({
180 unlink: function(fn){
181 return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn);
184 argvToString: (list)=>{
186 m.shift() /* strip command name */;
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)
198 utf8Encode: (str)=>__utf8Encoder.encode(str),
200 strglob: sqlite3.wasm.xWrap('sqlite3_wasm_SQLTester_strglob','int',
207 #logger = console.log.bind(console);
210 if(func) this.setFunc(func);
215 this.#logger = args[0];
222 if( this.getOutputPrefix && !this.#lnBuf.length ){
223 this.#lnBuf.push(this.getOutputPrefix());
225 this.#lnBuf.push(...args);
229 #outlnImpl(vLevel, ...args){
230 if( this.getOutputPrefix && !this.#lnBuf.length ){
231 this.#lnBuf.push(this.getOutputPrefix());
233 this.#lnBuf.push(...args,'\n');
234 const msg = this.#lnBuf.join('');
235 this.#lnBuf.length = 0;
241 return this.#outlnImpl(0,...args);
245 if( 0==arguments.length ){
246 return (this.getOutputPrefix
247 ? (this.getOutputPrefix() ?? '') : '');
249 this.getOutputPrefix = arguments[0];
254 static #verboseLabel = ["🔈",/*"🔉",*/"🔊","📢"];
256 if( this.#verbosity>=lvl ){
257 this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
260 verbose1(...args){ return this.verboseN(1,args); }
261 verbose2(...args){ return this.verboseN(2,args); }
262 verbose3(...args){ return this.verboseN(3,args); }
265 const rc = this.#verbosity;
266 if(arguments.length) this.#verbosity = +arguments[0];
274 //! Console output utility.
275 #outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
276 //! List of input scripts.
278 //! Test input buffer.
280 //! Test result buffer.
282 //! Output representation of SQL NULL.
287 //! Total test script files run
289 //! Test-case count for to the current TestScript
291 //! Names of scripts which were aborted.
294 #emitColNames = false;
295 //! True to keep going regardless of how a test fails.
298 //! The list of available db handles.
300 //! Index into this.list of the current db.
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];
316 outln(...args){ return this.#outer.outln(...args); }
317 out(...args){ return this.#outer.out(...args); }
320 this.#outer = args[0];
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); }
329 const rc = this.#outer.verbosity(...args);
330 return args.length ? this : rc;
333 this.#outer.logger(func);
337 incrementTestCounter(){
338 ++this.metrics.nTotalTest;
339 ++this.metrics.nTest;
343 this.clearInputBuffer();
344 this.clearResultBuffer();
345 this.#clearBuffer(this.#db.initSql);
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;");
354 appendInput(line, addNL){
355 this.#inputBuffer.push(line);
356 if( addNL ) this.#inputBuffer.push('\n');
358 appendResult(line, addNL){
359 this.#resultBuffer.push(line);
360 if( addNL ) this.#resultBuffer.push('\n');
362 appendDbInitSql(sql){
363 this.#db.initSql.push(sql);
364 if( this.currentDb() ){
365 this.execSql(null, true, ResultBufferMode.NONE, null, sql);
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);
379 #clearBuffer(buffer){
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(''); }
391 const s = buffer.join('');
397 return this.#takeBuffer(this.#inputBuffer);
400 return this.#takeBuffer(this.#resultBuffer);
404 return (0==arguments.length)
406 : (this.#nullView = ''+arguments[0]);
410 return (0==arguments.length)
412 : (this.#emitColNames = !!arguments[0]);
416 return (0==arguments.length)
417 ? this.#db.iCurrentDb
418 : (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
422 if(id<0 || id>=this.#db.list.length){
423 toss(SQLTesterException, "Database index ",id," is out of range.");
429 if( 0!=args.length ){
430 this.#affirmDbId(id).#db.iCurrentDb = id;
432 return this.#db.currentDb();
436 return this.#affirmDbId(id).#db.list[id];
439 getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
443 if( 0==arguments.length ){
444 id = this.#db.iCurrentDb;
446 const pDb = this.#affirmDbId(id).#db.list[id];
448 sqlite3.capi.sqlite3_close_v2(pDb);
449 this.#db.list[id] = null;
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;
460 this.#db.iCurrentDb = 0;
463 openDb(name, createIfNeeded){
464 if( 3===arguments.length ){
465 const slot = arguments[0];
466 this.#affirmDbId(slot).#db.iCurrentDb = slot;
468 createIfNeeded = arguments[2];
471 const capi = sqlite3.capi, wasm = sqlite3.wasm;
473 let flags = capi.SQLITE_OPEN_READWRITE;
474 if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
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);
483 if( 0==rc && this.#db.initSql.length > 0){
484 rc = this.#runInitSql(pDb);
487 sqlite3.SQLite3Error.toss(
489 "sqlite3 result code",rc+":",
490 (pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
491 : sqlite3.capi.sqlite3_errstr(rc))
494 return this.#db.list[this.#db.iCurrentDb] = pDb;
496 sqlite3.capi.sqlite3_close_v2(pDb);
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]));
509 if( !(ts instanceof TestScript) ){
510 Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
512 this.#aScripts.push(ts);
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){
524 ++this.metrics.nTestFile;
526 const timeStart = (new Date()).getTime();
531 if(e instanceof SQLTesterException){
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() ){
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();
550 this.out( "[",mod,"] " );
552 this.outln(ts.filename());
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
569 if( !this.#db.list[0] ){
570 Util.unlink(this.#db.initialDbName);
571 this.openDb(0, this.#db.initialDbName, true);
573 this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
574 "triggered while it is opened.");
579 if( !v ) return "{}";
580 if( !Rx.special.test(v) ){
581 return v /* no escaping needed */;
583 if( !Rx.squiggly.test(v) ){
588 for(let i = 0; i < n; ++i){
589 const ch = v.charAt(i);
591 case '\\': sb.push("\\\\"); break;
592 case '"': sb.push("\\\""); break;
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');
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) ){
612 sb.push('{', msg, '}');
617 sqlite3.oo1.DB.checkRc(pDb, rc);
620 execSql(pDb, throwOnError, appendMode, rowMode, sql){
621 if( !pDb && !this.#db.list[0] ){
622 this.#setupInitialDb();
624 if( !pDb ) pDb = this.#db.currentDb();
625 const wasm = sqlite3.wasm, capi = sqlite3.capi;
626 sql = (sql instanceof Uint8Array)
628 : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
630 const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
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 */)
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
651 throw new DbException(self, pDb, rc);
653 self.#appendDbErr(pDb, sb, rc);
657 const pStmt = wasm.peekPtr(ppStmt);
658 pSql = wasm.peekPtr(pzTail);
659 sqlByteLen = pSqlEnd - pSql;
660 if(!pStmt) continue /* only whitespace or comments */;
662 const nCol = capi.sqlite3_column_count(pStmt);
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);
670 case ResultBufferMode.ASIS: sb.push( colName ); break;
671 case ResultBufferMode.ESCAPED:
672 sb.push( self.#escapeSqlValue(colName) );
675 self.toss("Unhandled ResultBufferMode.");
679 val = capi.sqlite3_column_text(pStmt, i);
681 sb.push( self.#nullView );
685 case ResultBufferMode.ASIS: sb.push( val ); break;
686 case ResultBufferMode.ESCAPED:
687 sb.push( self.#escapeSqlValue(val) );
692 if( ResultRowMode.NEWLINE === rowMode ){
696 }else{ // no output but possibly other side effects
697 while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
699 capi.sqlite3_finalize(pStmt);
700 if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
703 self.#appendDbErr(db, sb, rc);
707 }/* SQL script loop */;
708 })/*scopedAllocCall()*/;
718 process(sqlTester,testScript,argv){
719 SQLTesterException.toss("process() must be overridden");
722 argcCheck(testScript,argv,min,max){
723 const argc = argv.length-1;
724 if(argc<min || (max>=0 && argc>max)){
726 testScript.toss(argv[0]," requires exactly ",min," argument(s)");
728 testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
730 testScript.toss(argv[0]," requires at least ",min," arguments.");
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.
745 //! Putback line number
747 //! Peeked-to pos, used by peekLine() and consumePeeked().
749 //! Peeked-to line number.
755 //! Restore parsing state to the start of the stream.
757 this.sb.length = this.pos = this.lineNo
758 = this.putbackPos = this.putbackLineNo
759 = this.peekedPos = this.peekedLineNo = 0;
764 #cursor = new Cursor();
767 #testCaseName = null;
768 #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
770 constructor(...args){
771 let content, filename;
772 if( 2 == args.length ){
775 }else if( 1 == args.length ){
776 if(args[0] instanceof Object){
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);
791 toss(Error, "Invalid content type for TestScript constructor.");
794 this.#filename = filename;
795 this.#cursor.src = content;
799 return (0==arguments.length)
800 ? this.#moduleName : (this.#moduleName = arguments[0]);
804 return (0==arguments.length)
805 ? this.#testCaseName : (this.#testCaseName = arguments[0]);
808 return (0==arguments.length)
809 ? this.#filename : (this.#filename = arguments[0]);
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;
820 this.#testCaseName = null;
821 this.#cursor.rewind();
826 throw new TestScriptFailed(this,...args);
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); }
833 const rc = this.#outer.verbosity(...args);
834 return args.length ? this : rc;
837 #checkRequiredProperties(tester, props){
838 if(true) return false;
840 for(const rp of props){
841 this.verbose2("REQUIRED_PROPERTIES: ",rp);
843 case "RECURSIVE_TRIGGERS":
844 tester.appendDbInitSql("pragma recursive_triggers=on;");
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;");
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;");
860 tester.appendDbInitSql("pragma auto_vacuum=full;");
864 tester.appendDbInitSql("pragma auto_vacuum=incremental;");
870 return props.length == nOk;
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);
879 let m = Rx.scriptModuleName.exec(line);
881 this.#moduleName = m[1];
884 m = Rx.requiredProperties.exec(line);
887 if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
888 throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
892 m = Rx.mixedModuleName.exec(line);
894 throw new IncompatibleDirective(this, m[1]+": "+m[3]);
896 if( line.indexOf("\n|")>=0 ){
897 throw new IncompatibleDirective(this, "newline-pipe combination.");
902 #getCommandArgv(line){
903 const m = Rx.command.exec(line);
904 return m ? m[1].trim().split(/\s+/) : null;
908 #isCommandLine(line, checkForImpl){
909 let m = Rx.command.exec(line);
910 if( m && checkForImpl ){
911 m = !!CommandDispatcher.getCommandByName(m[2]);
916 fetchCommandBody(tester){
919 while( (null !== (line = this.peekLine())) ){
920 this.#checkForDirective(tester, line);
921 if( this.#isCommandLine(line, true) ) break;
923 this.consumePeeked();
926 return !!line.trim() ? line : null;
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);
939 this.#processCommand(tester, argv);
942 tester.appendInput(line,true);
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);
953 CommandDispatcher.dispatch(tester, this, argv);
957 const cur = this.#cursor;
958 if( cur.pos==cur.src.byteLength ){
961 cur.putbackPos = cur.pos;
962 cur.putbackLineNo = cur.lineNo;
964 let b = 0, prevB = 0, i = cur.pos;
966 let nChar = 0 /* number of bytes in the aChar char */;
967 const end = cur.src.byteLength;
968 for(; i < end && !doBreak; ++i){
971 case 13/*CR*/: continue;
974 if(cur.sb.length>0) doBreak = true;
975 // Else it's an empty string
978 /* Multi-byte chars need to be gathered up and appended at
979 one time so that we can get them as string objects. */
982 case 0xC0: nChar = 2; break;
983 case 0xE0: nChar = 3; break;
984 case 0xF0: nChar = 4; break;
986 if( b > 127 ) this.toss("Invalid character (#"+b+").");
990 cur.sb.push(String.fromCharCode(b));
992 const aChar = [] /* multi-byte char buffer */;
993 for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
995 Util.utf8Decode( new Uint8Array(aChar) )
1004 const rv = cur.sb.join('');
1005 if( i==cur.src.byteLength && 0==rv.length ){
1006 return null /* EOF */;
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.
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;
1023 return this.getLine();
1025 cur.peekedPos = cur.pos;
1026 cur.peekedLineNo = cur.lineNo;
1028 cur.lineNo = oldLine;
1029 cur.putbackPos = oldPB;
1030 cur.putbackLineNo = oldPBL;
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().
1041 const cur = this.#cursor;
1042 cur.pos = cur.peekedPos;
1043 cur.lineNo = cur.peekedLineNo;
1047 Restores the cursor to the position it had before the previous
1051 const cur = this.#cursor;
1052 cur.pos = cur.putbackPos;
1053 cur.lineNo = cur.putbackLineNo;
1059 class CloseDbCommand extends Command {
1060 process(t, ts, argv){
1061 this.argcCheck(ts,argv,0,1);
1064 const arg = argv[1];
1065 if( "all" === arg ){
1073 id = t.currentDbId();
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]) );
1088 class DbCommand extends Command {
1089 process(t, ts, argv){
1090 this.argcCheck(ts,argv,1);
1091 t.currentDbId( parseInt(argv[1]) );
1096 class GlobCommand extends Command {
1098 constructor(negate=false){
1100 this.#negate = negate;
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);
1120 //! --notglob command
1121 class NotGlobCommand extends GlobCommand {
1122 constructor(){super(true);}
1126 class OpenDbCommand extends Command {
1127 #createIfNeeded = false;
1128 constructor(createIfNeeded=false){
1130 this.#createIfNeeded = createIfNeeded;
1132 process(t, ts, argv){
1133 this.argcCheck(ts,argv,1);
1134 t.openDb(argv[1], this.#createIfNeeded);
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){}
1149 class NullCommand extends Command {
1150 process(st, ts, argv){
1151 this.argcCheck(ts,argv,1);
1152 st.nullValue( argv[1] );
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() );
1163 st.outln( Util.argvToString(argv) );
1168 //! --result command
1169 class ResultCommand extends Command {
1171 constructor(resultBufferMode = ResultBufferMode.ESCAPED){
1173 this.#bufferMode = resultBufferMode;
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.");
1192 class JsonCommand extends ResultCommand {
1193 constructor(){ super(ResultBufferMode.ASIS); }
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);
1213 //! --tableresult command
1214 class TableResultCommand extends Command {
1216 constructor(jsonMode=false){
1218 this.#jsonMode = jsonMode;
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.");
1226 if( !body.endsWith("\n--end") ){
1227 ts.toss(argv[0], " must be terminated with --end\\n");
1229 body = body.substring(0, body.length-6);
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"),".");
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);
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: <<",
1254 }else if( 0!=Util.strglob(glob, res[i]) ){
1255 ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
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();
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]) );
1285 class CommandDispatcher {
1286 static map = newObj();
1288 static getCommandByName(name){
1289 let rv = CommandDispatcher.map[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;
1311 CommandDispatcher.map[name] = rv;
1316 static dispatch(tester, testScript, argv){
1317 const cmd = CommandDispatcher.getCommandByName(argv[0]);
1319 toss(UnknownCommand,testScript,argv[0]);
1321 cmd.process(tester, testScript, argv);
1323 }/*CommandDispatcher*/
1325 const namespace = newObj({
1328 IncompatibleDirective,
1339 export {namespace as default};