1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348 |
- /*
- ** 2023-08-29
- **
- ** The author disclaims copyright to this source code. In place of
- ** a legal notice, here is a blessing:
- **
- ** May you do good and not evil.
- ** May you find forgiveness for yourself and forgive others.
- ** May you share freely, never taking more than you give.
- **
- *************************************************************************
- ** This file contains the main application entry pointer for the JS
- ** implementation of the SQLTester framework.
- **
- ** This version is not well-documented because it's a direct port of
- ** the Java implementation, which is documented: in the main SQLite3
- ** source tree, see ext/jni/src/org/sqlite/jni/capi/SQLTester.java.
- */
- import sqlite3ApiInit from '/jswasm/sqlite3.mjs';
- const sqlite3 = await sqlite3ApiInit();
- const log = (...args)=>{
- console.log('SQLTester:',...args);
- };
- /**
- Try to install vfsName as the new default VFS. Once this succeeds
- (returns true) then it becomes a no-op on future calls. Throws if
- VFS registration as the default VFS fails but has no side effects
- if vfsName is not currently registered.
- */
- const tryInstallVfs = function f(vfsName){
- if(f.vfsName) return false;
- const pVfs = sqlite3.capi.sqlite3_vfs_find(vfsName);
- if(pVfs){
- log("Installing",'"'+vfsName+'"',"as default VFS.");
- const rc = sqlite3.capi.sqlite3_vfs_register(pVfs, 1);
- if(rc){
- sqlite3.SQLite3Error.toss(rc,"While trying to register",vfsName,"vfs.");
- }
- f.vfsName = vfsName;
- }
- return !!pVfs;
- };
- tryInstallVfs.vfsName = undefined;
- if( 0 && globalThis.WorkerGlobalScope ){
- // Try OPFS storage, if available...
- if( 1 && sqlite3.oo1.OpfsDb ){
- /* Really slow with these tests */
- tryInstallVfs("opfs");
- }else if( sqlite3.installOpfsSAHPoolVfs ){
- await sqlite3.installOpfsSAHPoolVfs({
- clearOnInit: true,
- initialCapacity: 15,
- name: 'opfs-SQLTester'
- }).then(pool=>{
- tryInstallVfs(pool.vfsName);
- }).catch(e=>{
- log("OpfsSAHPool could not load:",e);
- });
- }
- }
- const wPost = (function(){
- return (('undefined'===typeof WorkerGlobalScope)
- ? ()=>{}
- : (type, payload)=>{
- postMessage({type, payload});
- });
- })();
- //log("WorkerGlobalScope",globalThis.WorkerGlobalScope);
- // Return a new enum entry value
- const newE = ()=>Object.create(null);
- const newObj = (props)=>Object.assign(newE(), props);
- /**
- Modes for how to escape (or not) column values and names from
- SQLTester.execSql() to the result buffer output.
- */
- const ResultBufferMode = Object.assign(Object.create(null),{
- //! Do not append to result buffer
- NONE: newE(),
- //! Append output escaped.
- ESCAPED: newE(),
- //! Append output as-is
- ASIS: newE()
- });
- /**
- Modes to specify how to emit multi-row output from
- SQLTester.execSql() to the result buffer.
- */
- const ResultRowMode = newObj({
- //! Keep all result rows on one line, space-separated.
- ONLINE: newE(),
- //! Add a newline between each result row.
- NEWLINE: newE()
- });
- class SQLTesterException extends globalThis.Error {
- constructor(testScript, ...args){
- if(testScript){
- super( [testScript.getOutputPrefix()+": ", ...args].join('') );
- }else{
- super( args.join('') );
- }
- this.name = 'SQLTesterException';
- }
- isFatal() { return false; }
- }
- SQLTesterException.toss = (...args)=>{
- throw new SQLTesterException(...args);
- }
- class DbException extends SQLTesterException {
- constructor(testScript, pDb, rc, closeDb=false){
- super(testScript, "DB error #"+rc+": "+sqlite3.capi.sqlite3_errmsg(pDb));
- this.name = 'DbException';
- if( closeDb ) sqlite3.capi.sqlite3_close_v2(pDb);
- }
- isFatal() { return true; }
- }
- class TestScriptFailed extends SQLTesterException {
- constructor(testScript, ...args){
- super(testScript,...args);
- this.name = 'TestScriptFailed';
- }
- isFatal() { return true; }
- }
- class UnknownCommand extends SQLTesterException {
- constructor(testScript, cmdName){
- super(testScript, cmdName);
- this.name = 'UnknownCommand';
- }
- isFatal() { return true; }
- }
- class IncompatibleDirective extends SQLTesterException {
- constructor(testScript, ...args){
- super(testScript,...args);
- this.name = 'IncompatibleDirective';
- }
- }
- //! For throwing where an expression is required.
- const toss = (errType, ...args)=>{
- throw new errType(...args);
- };
- const __utf8Decoder = new TextDecoder();
- const __utf8Encoder = new TextEncoder('utf-8');
- //! Workaround for Util.utf8Decode()
- const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer)
- ? function(){} : globalThis.SharedArrayBuffer;
- /* Frequently-reused regexes. */
- const Rx = newObj({
- requiredProperties: / REQUIRED_PROPERTIES:[ \t]*(\S.*)\s*$/,
- scriptModuleName: / SCRIPT_MODULE_NAME:[ \t]*(\S+)\s*$/,
- mixedModuleName: / ((MIXED_)?MODULE_NAME):[ \t]*(\S+)\s*$/,
- command: /^--(([a-z-]+)( .*)?)$/,
- //! "Special" characters - we have to escape output if it contains any.
- special: /[\x00-\x20\x22\x5c\x7b\x7d]/,
- squiggly: /[{}]/
- });
- const Util = newObj({
- toss,
- unlink: function f(fn){
- if(!f.unlink){
- f.unlink = sqlite3.wasm.xWrap('sqlite3__wasm_vfs_unlink','int',
- ['*','string']);
- }
- return 0==f.unlink(0,fn);
- },
- argvToString: (list)=>{
- const m = [...list];
- m.shift() /* strip command name */;
- return m.join(" ")
- },
- utf8Decode: function(arrayBuffer, begin, end){
- return __utf8Decoder.decode(
- (arrayBuffer.buffer instanceof __SAB)
- ? arrayBuffer.slice(begin, end)
- : arrayBuffer.subarray(begin, end)
- );
- },
- utf8Encode: (str)=>__utf8Encoder.encode(str),
- strglob: sqlite3.wasm.xWrap('sqlite3__wasm_SQLTester_strglob','int',
- ['string','string'])
- })/*Util*/;
- /**
- Output logger utility.
- */
- class Outer {
- #lnBuf = [];
- #verbosity = 0;
- #logger = console.log.bind(console);
- constructor(func){
- if(func) this.setFunc(func);
- }
- logger(...args){
- if(args.length){
- this.#logger = args[0];
- return this;
- }
- return this.#logger;
- }
- out(...args){
- if( this.getOutputPrefix && !this.#lnBuf.length ){
- this.#lnBuf.push(this.getOutputPrefix());
- }
- this.#lnBuf.push(...args);
- return this;
- }
- #outlnImpl(vLevel, ...args){
- if( this.getOutputPrefix && !this.#lnBuf.length ){
- this.#lnBuf.push(this.getOutputPrefix());
- }
- this.#lnBuf.push(...args,'\n');
- const msg = this.#lnBuf.join('');
- this.#lnBuf.length = 0;
- this.#logger(msg);
- return this;
- }
- outln(...args){
- return this.#outlnImpl(0,...args);
- }
- outputPrefix(){
- if( 0==arguments.length ){
- return (this.getOutputPrefix
- ? (this.getOutputPrefix() ?? '') : '');
- }else{
- this.getOutputPrefix = arguments[0];
- return this;
- }
- }
- static #verboseLabel = ["🔈",/*"🔉",*/"🔊","📢"];
- verboseN(lvl, args){
- if( this.#verbosity>=lvl ){
- this.#outlnImpl(lvl, Outer.#verboseLabel[lvl-1],': ',...args);
- }
- }
- verbose1(...args){ return this.verboseN(1,args); }
- verbose2(...args){ return this.verboseN(2,args); }
- verbose3(...args){ return this.verboseN(3,args); }
- verbosity(){
- const rc = this.#verbosity;
- if(arguments.length) this.#verbosity = +arguments[0];
- return rc;
- }
- }/*Outer*/
- class SQLTester {
- //! Console output utility.
- #outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
- //! List of input scripts.
- #aScripts = [];
- //! Test input buffer.
- #inputBuffer = [];
- //! Test result buffer.
- #resultBuffer = [];
- //! Output representation of SQL NULL.
- #nullView;
- metrics = newObj({
- //! Total tests run
- nTotalTest: 0,
- //! Total test script files run
- nTestFile: 0,
- //! Test-case count for to the current TestScript
- nTest: 0,
- //! Names of scripts which were aborted.
- failedScripts: []
- });
- #emitColNames = false;
- //! True to keep going regardless of how a test fails.
- #keepGoing = false;
- #db = newObj({
- //! The list of available db handles.
- list: new Array(7),
- //! Index into this.list of the current db.
- iCurrentDb: 0,
- //! Name of the default db, re-created for each script.
- initialDbName: "test.db",
- //! Buffer for REQUIRED_PROPERTIES pragmas.
- initSql: ['select 1;'],
- //! (sqlite3*) to the current db.
- currentDb: function(){
- return this.list[this.iCurrentDb];
- }
- });
- constructor(){
- this.reset();
- }
- outln(...args){ return this.#outer.outln(...args); }
- out(...args){ return this.#outer.out(...args); }
- outer(...args){
- if(args.length){
- this.#outer = args[0];
- return this;
- }
- return this.#outer;
- }
- verbose1(...args){ return this.#outer.verboseN(1,args); }
- verbose2(...args){ return this.#outer.verboseN(2,args); }
- verbose3(...args){ return this.#outer.verboseN(3,args); }
- verbosity(...args){
- const rc = this.#outer.verbosity(...args);
- return args.length ? this : rc;
- }
- setLogger(func){
- this.#outer.logger(func);
- return this;
- }
- incrementTestCounter(){
- ++this.metrics.nTotalTest;
- ++this.metrics.nTest;
- }
- reset(){
- this.clearInputBuffer();
- this.clearResultBuffer();
- this.#clearBuffer(this.#db.initSql);
- this.closeAllDbs();
- this.metrics.nTest = 0;
- this.#nullView = "nil";
- this.#emitColNames = false;
- this.#db.iCurrentDb = 0;
- //this.#db.initSql.push("SELECT 1;");
- }
- appendInput(line, addNL){
- this.#inputBuffer.push(line);
- if( addNL ) this.#inputBuffer.push('\n');
- }
- appendResult(line, addNL){
- this.#resultBuffer.push(line);
- if( addNL ) this.#resultBuffer.push('\n');
- }
- appendDbInitSql(sql){
- this.#db.initSql.push(sql);
- if( this.currentDb() ){
- this.execSql(null, true, ResultBufferMode.NONE, null, sql);
- }
- }
- #runInitSql(pDb){
- let rc = 0;
- for(const sql of this.#db.initSql){
- this.#outer.verbose2("RUNNING DB INIT CODE: ",sql);
- rc = this.execSql(pDb, false, ResultBufferMode.NONE, null, sql);
- if( rc ) break;
- }
- return rc;
- }
- #clearBuffer(buffer){
- buffer.length = 0;
- return buffer;
- }
- clearInputBuffer(){ return this.#clearBuffer(this.#inputBuffer); }
- clearResultBuffer(){return this.#clearBuffer(this.#resultBuffer); }
- getInputText(){ return this.#inputBuffer.join(''); }
- getResultText(){ return this.#resultBuffer.join(''); }
- #takeBuffer(buffer){
- const s = buffer.join('');
- buffer.length = 0;
- return s;
- }
- takeInputBuffer(){
- return this.#takeBuffer(this.#inputBuffer);
- }
- takeResultBuffer(){
- return this.#takeBuffer(this.#resultBuffer);
- }
- nullValue(){
- return (0==arguments.length)
- ? this.#nullView
- : (this.#nullView = ''+arguments[0]);
- }
- outputColumnNames(){
- return (0==arguments.length)
- ? this.#emitColNames
- : (this.#emitColNames = !!arguments[0]);
- }
- currentDbId(){
- return (0==arguments.length)
- ? this.#db.iCurrentDb
- : (this.#affirmDbId(arguments[0]).#db.iCurrentDb = arguments[0]);
- }
- #affirmDbId(id){
- if(id<0 || id>=this.#db.list.length){
- toss(SQLTesterException, "Database index ",id," is out of range.");
- }
- return this;
- }
- currentDb(...args){
- if( 0!=args.length ){
- this.#affirmDbId(id).#db.iCurrentDb = id;
- }
- return this.#db.currentDb();
- }
- getDbById(id){
- return this.#affirmDbId(id).#db.list[id];
- }
- getCurrentDb(){ return this.#db.list[this.#db.iCurrentDb]; }
- closeDb(id) {
- if( 0==arguments.length ){
- id = this.#db.iCurrentDb;
- }
- const pDb = this.#affirmDbId(id).#db.list[id];
- if( pDb ){
- sqlite3.capi.sqlite3_close_v2(pDb);
- this.#db.list[id] = null;
- }
- }
- closeAllDbs(){
- for(let i = 0; i<this.#db.list.length; ++i){
- if(this.#db.list[i]){
- sqlite3.capi.sqlite3_close_v2(this.#db.list[i]);
- this.#db.list[i] = null;
- }
- }
- this.#db.iCurrentDb = 0;
- }
- openDb(name, createIfNeeded){
- if( 3===arguments.length ){
- const slot = arguments[0];
- this.#affirmDbId(slot).#db.iCurrentDb = slot;
- name = arguments[1];
- createIfNeeded = arguments[2];
- }
- this.closeDb();
- const capi = sqlite3.capi, wasm = sqlite3.wasm;
- let pDb = 0;
- let flags = capi.SQLITE_OPEN_READWRITE;
- if( createIfNeeded ) flags |= capi.SQLITE_OPEN_CREATE;
- try{
- let rc;
- wasm.pstack.call(function(){
- let ppOut = wasm.pstack.allocPtr();
- rc = sqlite3.capi.sqlite3_open_v2(name, ppOut, flags, null);
- pDb = wasm.peekPtr(ppOut);
- });
- let sql;
- if( 0==rc && this.#db.initSql.length > 0){
- rc = this.#runInitSql(pDb);
- }
- if( 0!=rc ){
- sqlite3.SQLite3Error.toss(
- rc,
- "sqlite3 result code",rc+":",
- (pDb ? sqlite3.capi.sqlite3_errmsg(pDb)
- : sqlite3.capi.sqlite3_errstr(rc))
- );
- }
- return this.#db.list[this.#db.iCurrentDb] = pDb;
- }catch(e){
- sqlite3.capi.sqlite3_close_v2(pDb);
- throw e;
- }
- }
- addTestScript(ts){
- if( 2===arguments.length ){
- ts = new TestScript(arguments[0], arguments[1]);
- }else if(ts instanceof Uint8Array){
- ts = new TestScript('<unnamed>', ts);
- }else if('string' === typeof arguments[1]){
- ts = new TestScript('<unnamed>', Util.utf8Encode(arguments[1]));
- }
- if( !(ts instanceof TestScript) ){
- Util.toss(SQLTesterException, "Invalid argument type for addTestScript()");
- }
- this.#aScripts.push(ts);
- return this;
- }
- runTests(){
- const tStart = (new Date()).getTime();
- let isVerbose = this.verbosity();
- this.metrics.failedScripts.length = 0;
- this.metrics.nTotalTest = 0;
- this.metrics.nTestFile = 0;
- for(const ts of this.#aScripts){
- this.reset();
- ++this.metrics.nTestFile;
- let threw = false;
- const timeStart = (new Date()).getTime();
- let msgTail = '';
- try{
- ts.run(this);
- }catch(e){
- if(e instanceof SQLTesterException){
- threw = true;
- this.outln("🔥EXCEPTION: ",e);
- this.metrics.failedScripts.push({script: ts.filename(), message:e.toString()});
- if( this.#keepGoing ){
- this.outln("Continuing anyway because of the keep-going option.");
- }else if( e.isFatal() ){
- throw e;
- }
- }else{
- throw e;
- }
- }finally{
- const timeEnd = (new Date()).getTime();
- this.out("🏁", (threw ? "❌" : "✅"), " ",
- this.metrics.nTest, " test(s) in ",
- (timeEnd-timeStart),"ms. ");
- const mod = ts.moduleName();
- if( mod ){
- this.out( "[",mod,"] " );
- }
- this.outln(ts.filename());
- }
- }
- const tEnd = (new Date()).getTime();
- Util.unlink(this.#db.initialDbName);
- this.outln("Took ",(tEnd-tStart),"ms. Test count = ",
- this.metrics.nTotalTest,", script count = ",
- this.#aScripts.length,(
- this.metrics.failedScripts.length
- ? ", failed scripts = "+this.metrics.failedScripts.length
- : ""
- )
- );
- return this;
- }
- #setupInitialDb(){
- if( !this.#db.list[0] ){
- Util.unlink(this.#db.initialDbName);
- this.openDb(0, this.#db.initialDbName, true);
- }else{
- this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
- "triggered while it is opened.");
- }
- }
- #escapeSqlValue(v){
- if( !v ) return "{}";
- if( !Rx.special.test(v) ){
- return v /* no escaping needed */;
- }
- if( !Rx.squiggly.test(v) ){
- return "{"+v+"}";
- }
- const sb = ["\""];
- const n = v.length;
- for(let i = 0; i < n; ++i){
- const ch = v.charAt(i);
- switch(ch){
- case '\\': sb.push("\\\\"); break;
- case '"': sb.push("\\\""); break;
- default:{
- //verbose("CHAR ",(int)ch," ",ch," octal=",String.format("\\%03o", (int)ch));
- const ccode = ch.charCodeAt(i);
- if( ccode < 32 ) sb.push('\\',ccode.toString(8),'o');
- else sb.push(ch);
- break;
- }
- }
- }
- sb.push("\"");
- return sb.join('');
- }
- #appendDbErr(pDb, sb, rc){
- sb.push(sqlite3.capi.sqlite3_js_rc_str(rc), ' ');
- const msg = this.#escapeSqlValue(sqlite3.capi.sqlite3_errmsg(pDb));
- if( '{' === msg.charAt(0) ){
- sb.push(msg);
- }else{
- sb.push('{', msg, '}');
- }
- }
- #checkDbRc(pDb,rc){
- sqlite3.oo1.DB.checkRc(pDb, rc);
- }
- execSql(pDb, throwOnError, appendMode, rowMode, sql){
- if( !pDb && !this.#db.list[0] ){
- this.#setupInitialDb();
- }
- if( !pDb ) pDb = this.#db.currentDb();
- const wasm = sqlite3.wasm, capi = sqlite3.capi;
- sql = (sql instanceof Uint8Array)
- ? sql
- : Util.utf8Encode(capi.sqlite3_js_sql_to_string(sql));
- const self = this;
- const sb = (ResultBufferMode.NONE===appendMode) ? null : this.#resultBuffer;
- let rc = 0;
- wasm.scopedAllocCall(function(){
- let sqlByteLen = sql.byteLength;
- const ppStmt = wasm.scopedAlloc(
- /* output (sqlite3_stmt**) arg and pzTail */
- (2 * wasm.ptrSizeof) + (sqlByteLen + 1/* SQL + NUL */)
- );
- const pzTail = ppStmt + wasm.ptrSizeof /* final arg to sqlite3_prepare_v2() */;
- let pSql = pzTail + wasm.ptrSizeof;
- const pSqlEnd = pSql + sqlByteLen;
- wasm.heap8().set(sql, pSql);
- wasm.poke8(pSql + sqlByteLen, 0/*NUL terminator*/);
- let pos = 0, n = 1, spacing = 0;
- while( pSql && wasm.peek8(pSql) ){
- wasm.pokePtr([ppStmt, pzTail], 0);
- rc = capi.sqlite3_prepare_v3(
- pDb, pSql, sqlByteLen, 0, ppStmt, pzTail
- );
- if( 0!==rc ){
- if(throwOnError){
- throw new DbException(self, pDb, rc);
- }else if( sb ){
- self.#appendDbErr(pDb, sb, rc);
- }
- break;
- }
- const pStmt = wasm.peekPtr(ppStmt);
- pSql = wasm.peekPtr(pzTail);
- sqlByteLen = pSqlEnd - pSql;
- if(!pStmt) continue /* only whitespace or comments */;
- if( sb ){
- const nCol = capi.sqlite3_column_count(pStmt);
- let colName, val;
- while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {
- for( let i=0; i < nCol; ++i ){
- if( spacing++ > 0 ) sb.push(' ');
- if( self.#emitColNames ){
- colName = capi.sqlite3_column_name(pStmt, i);
- switch(appendMode){
- case ResultBufferMode.ASIS: sb.push( colName ); break;
- case ResultBufferMode.ESCAPED:
- sb.push( self.#escapeSqlValue(colName) );
- break;
- default:
- self.toss("Unhandled ResultBufferMode.");
- }
- sb.push(' ');
- }
- val = capi.sqlite3_column_text(pStmt, i);
- if( null===val ){
- sb.push( self.#nullView );
- continue;
- }
- switch(appendMode){
- case ResultBufferMode.ASIS: sb.push( val ); break;
- case ResultBufferMode.ESCAPED:
- sb.push( self.#escapeSqlValue(val) );
- break;
- }
- }/* column loop */
- if( ResultRowMode.NEWLINE === rowMode ){
- spacing = 0;
- sb.push('\n');
- }
- }/* row loop */
- }else{ // no output but possibly other side effects
- while( capi.SQLITE_ROW === (rc = capi.sqlite3_step(pStmt)) ) {}
- }
- capi.sqlite3_finalize(pStmt);
- if( capi.SQLITE_ROW===rc || capi.SQLITE_DONE===rc) rc = 0;
- else if( rc!=0 ){
- if( sb ){
- self.#appendDbErr(pDb, sb, rc);
- }
- break;
- }
- }/* SQL script loop */;
- })/*scopedAllocCall()*/;
- return rc;
- }
- }/*SQLTester*/
- class Command {
- constructor(){
- }
- process(sqlTester,testScript,argv){
- SQLTesterException.toss("process() must be overridden");
- }
- argcCheck(testScript,argv,min,max){
- const argc = argv.length-1;
- if(argc<min || (max>=0 && argc>max)){
- if( min==max ){
- testScript.toss(argv[0]," requires exactly ",min," argument(s)");
- }else if(max>0){
- testScript.toss(argv[0]," requires ",min,"-",max," arguments.");
- }else{
- testScript.toss(argv[0]," requires at least ",min," arguments.");
- }
- }
- }
- }
- class Cursor {
- src;
- sb = [];
- pos = 0;
- //! Current line number. Starts at 0 for internal reasons and will
- // line up with 1-based reality once parsing starts.
- lineNo = 0 /* yes, zero */;
- //! Putback value for this.pos.
- putbackPos = 0;
- //! Putback line number
- putbackLineNo = 0;
- //! Peeked-to pos, used by peekLine() and consumePeeked().
- peekedPos = 0;
- //! Peeked-to line number.
- peekedLineNo = 0;
- constructor(){
- }
- //! Restore parsing state to the start of the stream.
- rewind(){
- this.sb.length = this.pos = this.lineNo
- = this.putbackPos = this.putbackLineNo
- = this.peekedPos = this.peekedLineNo = 0;
- }
- }
- class TestScript {
- #cursor = new Cursor();
- #moduleName = null;
- #filename = null;
- #testCaseName = null;
- #outer = new Outer().outputPrefix( ()=>this.getOutputPrefix()+': ' );
- constructor(...args){
- let content, filename;
- if( 2 == args.length ){
- filename = args[0];
- content = args[1];
- }else if( 1 == args.length ){
- if(args[0] instanceof Object){
- const o = args[0];
- filename = o.name;
- content = o.content;
- }else{
- content = args[0];
- }
- }
- if(!(content instanceof Uint8Array)){
- if('string' === typeof content){
- content = Util.utf8Encode(content);
- }else if((content instanceof ArrayBuffer)
- ||(content instanceof Array)){
- content = new Uint8Array(content);
- }else{
- toss(Error, "Invalid content type for TestScript constructor.");
- }
- }
- this.#filename = filename;
- this.#cursor.src = content;
- }
- moduleName(){
- return (0==arguments.length)
- ? this.#moduleName : (this.#moduleName = arguments[0]);
- }
- testCaseName(){
- return (0==arguments.length)
- ? this.#testCaseName : (this.#testCaseName = arguments[0]);
- }
- filename(){
- return (0==arguments.length)
- ? this.#filename : (this.#filename = arguments[0]);
- }
- getOutputPrefix() {
- let rc = "["+(this.#moduleName || '<unnamed>')+"]";
- if( this.#testCaseName ) rc += "["+this.#testCaseName+"]";
- if( this.#filename ) rc += '['+this.#filename+']';
- return rc + " line "+ this.#cursor.lineNo;
- }
- reset(){
- this.#testCaseName = null;
- this.#cursor.rewind();
- return this;
- }
- toss(...args){
- throw new TestScriptFailed(this,...args);
- }
- verbose1(...args){ return this.#outer.verboseN(1,args); }
- verbose2(...args){ return this.#outer.verboseN(2,args); }
- verbose3(...args){ return this.#outer.verboseN(3,args); }
- verbosity(...args){
- const rc = this.#outer.verbosity(...args);
- return args.length ? this : rc;
- }
- #checkRequiredProperties(tester, props){
- if(true) return false;
- let nOk = 0;
- for(const rp of props){
- this.verbose2("REQUIRED_PROPERTIES: ",rp);
- switch(rp){
- case "RECURSIVE_TRIGGERS":
- tester.appendDbInitSql("pragma recursive_triggers=on;");
- ++nOk;
- break;
- case "TEMPSTORE_FILE":
- /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
- which we just happen to know is the case */
- tester.appendDbInitSql("pragma temp_store=1;");
- ++nOk;
- break;
- case "TEMPSTORE_MEM":
- /* This _assumes_ that the lib is built with SQLITE_TEMP_STORE=1 or 2,
- which we just happen to know is the case */
- tester.appendDbInitSql("pragma temp_store=0;");
- ++nOk;
- break;
- case "AUTOVACUUM":
- tester.appendDbInitSql("pragma auto_vacuum=full;");
- ++nOk;
- break;
- case "INCRVACUUM":
- tester.appendDbInitSql("pragma auto_vacuum=incremental;");
- ++nOk;
- default:
- break;
- }
- }
- return props.length == nOk;
- }
- #checkForDirective(tester,line){
- if(line.startsWith("#")){
- throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
- }else if(line.startsWith("---")){
- throw new IncompatibleDirective(this, "triple-dash: ",line);
- }
- let m = Rx.scriptModuleName.exec(line);
- if( m ){
- this.#moduleName = m[1];
- return;
- }
- m = Rx.requiredProperties.exec(line);
- if( m ){
- const rp = m[1];
- if( !this.#checkRequiredProperties( tester, rp.split(/\s+/).filter(v=>!!v) ) ){
- throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+rp);
- }
- }
- m = Rx.mixedModuleName.exec(line);
- if( m ){
- throw new IncompatibleDirective(this, m[1]+": "+m[3]);
- }
- if( line.indexOf("\n|")>=0 ){
- throw new IncompatibleDirective(this, "newline-pipe combination.");
- }
- }
- #getCommandArgv(line){
- const m = Rx.command.exec(line);
- return m ? m[1].trim().split(/\s+/) : null;
- }
- #isCommandLine(line, checkForImpl){
- let m = Rx.command.exec(line);
- if( m && checkForImpl ){
- m = !!CommandDispatcher.getCommandByName(m[2]);
- }
- return !!m;
- }
- fetchCommandBody(tester){
- const sb = [];
- let line;
- while( (null !== (line = this.peekLine())) ){
- this.#checkForDirective(tester, line);
- if( this.#isCommandLine(line, true) ) break;
- sb.push(line,"\n");
- this.consumePeeked();
- }
- line = sb.join('');
- return !!line.trim() ? line : null;
- }
- run(tester){
- this.reset();
- this.#outer.verbosity( tester.verbosity() );
- this.#outer.logger( tester.outer().logger() );
- let line, directive, argv = [];
- while( null != (line = this.getLine()) ){
- this.verbose3("run() input line: ",line);
- this.#checkForDirective(tester, line);
- argv = this.#getCommandArgv(line);
- if( argv ){
- this.#processCommand(tester, argv);
- continue;
- }
- tester.appendInput(line,true);
- }
- return true;
- }
- #processCommand(tester, argv){
- this.verbose2("processCommand(): ",argv[0], " ", Util.argvToString(argv));
- if(this.#outer.verbosity()>1){
- const input = tester.getInputText();
- this.verbose3("processCommand() input buffer = ",input);
- }
- CommandDispatcher.dispatch(tester, this, argv);
- }
- getLine(){
- const cur = this.#cursor;
- if( cur.pos==cur.src.byteLength ){
- return null/*EOF*/;
- }
- cur.putbackPos = cur.pos;
- cur.putbackLineNo = cur.lineNo;
- cur.sb.length = 0;
- let b = 0, prevB = 0, i = cur.pos;
- let doBreak = false;
- let nChar = 0 /* number of bytes in the aChar char */;
- const end = cur.src.byteLength;
- for(; i < end && !doBreak; ++i){
- b = cur.src[i];
- switch( b ){
- case 13/*CR*/: continue;
- case 10/*NL*/:
- ++cur.lineNo;
- if(cur.sb.length>0) doBreak = true;
- // Else it's an empty string
- break;
- default:{
- /* Multi-byte chars need to be gathered up and appended at
- one time so that we can get them as string objects. */
- nChar = 1;
- switch( b & 0xF0 ){
- case 0xC0: nChar = 2; break;
- case 0xE0: nChar = 3; break;
- case 0xF0: nChar = 4; break;
- default:
- if( b > 127 ) this.toss("Invalid character (#"+b+").");
- break;
- }
- if( 1==nChar ){
- cur.sb.push(String.fromCharCode(b));
- }else{
- const aChar = [] /* multi-byte char buffer */;
- for(let x = 0; (x < nChar) && (i+x < end); ++x) aChar[x] = cur.src[i+x];
- cur.sb.push(
- Util.utf8Decode( new Uint8Array(aChar) )
- );
- i += nChar-1;
- }
- break;
- }
- }
- }
- cur.pos = i;
- const rv = cur.sb.join('');
- if( i==cur.src.byteLength && 0==rv.length ){
- return null /* EOF */;
- }
- return rv;
- }/*getLine()*/
- /**
- Fetches the next line then resets the cursor to its pre-call
- state. consumePeeked() can be used to consume this peeked line
- without having to re-parse it.
- */
- peekLine(){
- const cur = this.#cursor;
- const oldPos = cur.pos;
- const oldPB = cur.putbackPos;
- const oldPBL = cur.putbackLineNo;
- const oldLine = cur.lineNo;
- try {
- return this.getLine();
- }finally{
- cur.peekedPos = cur.pos;
- cur.peekedLineNo = cur.lineNo;
- cur.pos = oldPos;
- cur.lineNo = oldLine;
- cur.putbackPos = oldPB;
- cur.putbackLineNo = oldPBL;
- }
- }
- /**
- Only valid after calling peekLine() and before calling getLine().
- This places the cursor to the position it would have been at had
- the peekLine() had been fetched with getLine().
- */
- consumePeeked(){
- const cur = this.#cursor;
- cur.pos = cur.peekedPos;
- cur.lineNo = cur.peekedLineNo;
- }
- /**
- Restores the cursor to the position it had before the previous
- call to getLine().
- */
- putbackLine(){
- const cur = this.#cursor;
- cur.pos = cur.putbackPos;
- cur.lineNo = cur.putbackLineNo;
- }
- }/*TestScript*/;
- //! --close command
- class CloseDbCommand extends Command {
- process(t, ts, argv){
- this.argcCheck(ts,argv,0,1);
- let id;
- if(argv.length>1){
- const arg = argv[1];
- if( "all" === arg ){
- t.closeAllDbs();
- return;
- }
- else{
- id = parseInt(arg);
- }
- }else{
- id = t.currentDbId();
- }
- t.closeDb(id);
- }
- }
- //! --column-names command
- class ColumnNamesCommand extends Command {
- process( st, ts, argv ){
- this.argcCheck(ts,argv,1);
- st.outputColumnNames( !!parseInt(argv[1]) );
- }
- }
- //! --db command
- class DbCommand extends Command {
- process(t, ts, argv){
- this.argcCheck(ts,argv,1);
- t.currentDbId( parseInt(argv[1]) );
- }
- }
- //! --glob command
- class GlobCommand extends Command {
- #negate = false;
- constructor(negate=false){
- super();
- this.#negate = negate;
- }
- process(t, ts, argv){
- this.argcCheck(ts,argv,1,-1);
- t.incrementTestCounter();
- const sql = t.takeInputBuffer();
- let rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
- ResultRowMode.ONELINE, sql);
- const result = t.getResultText();
- const sArgs = Util.argvToString(argv);
- //t2.verbose2(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
- const glob = Util.argvToString(argv);
- rc = Util.strglob(glob, result);
- if( (this.#negate && 0===rc) || (!this.#negate && 0!==rc) ){
- ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
- }
- }
- }
- //! --notglob command
- class NotGlobCommand extends GlobCommand {
- constructor(){super(true);}
- }
- //! --open command
- class OpenDbCommand extends Command {
- #createIfNeeded = false;
- constructor(createIfNeeded=false){
- super();
- this.#createIfNeeded = createIfNeeded;
- }
- process(t, ts, argv){
- this.argcCheck(ts,argv,1);
- t.openDb(argv[1], this.#createIfNeeded);
- }
- }
- //! --new command
- class NewDbCommand extends OpenDbCommand {
- constructor(){ super(true); }
- }
- //! Placeholder dummy/no-op commands
- class NoopCommand extends Command {
- process(t, ts, argv){}
- }
- //! --null command
- class NullCommand extends Command {
- process(st, ts, argv){
- this.argcCheck(ts,argv,1);
- st.nullValue( argv[1] );
- }
- }
- //! --print command
- class PrintCommand extends Command {
- process(st, ts, argv){
- st.out(ts.getOutputPrefix(),': ');
- if( 1==argv.length ){
- st.out( st.getInputText() );
- }else{
- st.outln( Util.argvToString(argv) );
- }
- }
- }
- //! --result command
- class ResultCommand extends Command {
- #bufferMode;
- constructor(resultBufferMode = ResultBufferMode.ESCAPED){
- super();
- this.#bufferMode = resultBufferMode;
- }
- process(t, ts, argv){
- this.argcCheck(ts,argv,0,-1);
- t.incrementTestCounter();
- const sql = t.takeInputBuffer();
- //ts.verbose2(argv[0]," SQL =\n",sql);
- t.execSql(null, false, this.#bufferMode, ResultRowMode.ONELINE, sql);
- const result = t.getResultText().trim();
- const sArgs = argv.length>1 ? Util.argvToString(argv) : "";
- if( result !== sArgs ){
- t.outln(argv[0]," FAILED comparison. Result buffer:\n",
- result,"\nExpected result:\n",sArgs);
- ts.toss(argv[0]+" comparison failed.");
- }
- }
- }
- //! --json command
- class JsonCommand extends ResultCommand {
- constructor(){ super(ResultBufferMode.ASIS); }
- }
- //! --run command
- class RunCommand extends Command {
- process(t, ts, argv){
- this.argcCheck(ts,argv,0,1);
- const pDb = (1==argv.length)
- ? t.currentDb() : t.getDbById( parseInt(argv[1]) );
- const sql = t.takeInputBuffer();
- const rc = t.execSql(pDb, false, ResultBufferMode.NONE,
- ResultRowMode.ONELINE, sql);
- if( 0!==rc && t.verbosity()>0 ){
- const msg = sqlite3.capi.sqlite3_errmsg(pDb);
- ts.verbose2(argv[0]," non-fatal command error #",rc,": ",
- msg,"\nfor SQL:\n",sql);
- }
- }
- }
- //! --tableresult command
- class TableResultCommand extends Command {
- #jsonMode;
- constructor(jsonMode=false){
- super();
- this.#jsonMode = jsonMode;
- }
- process(t, ts, argv){
- this.argcCheck(ts,argv,0);
- t.incrementTestCounter();
- let body = ts.fetchCommandBody(t);
- if( null===body ) ts.toss("Missing ",argv[0]," body.");
- body = body.trim();
- if( !body.endsWith("\n--end") ){
- ts.toss(argv[0], " must be terminated with --end\\n");
- }else{
- body = body.substring(0, body.length-6);
- }
- const globs = body.split(/\s*\n\s*/);
- if( globs.length < 1 ){
- ts.toss(argv[0], " requires 1 or more ",
- (this.#jsonMode ? "json snippets" : "globs"),".");
- }
- const sql = t.takeInputBuffer();
- t.execSql(null, true,
- this.#jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
- ResultRowMode.NEWLINE, sql);
- const rbuf = t.getResultText().trim();
- const res = rbuf.split(/\r?\n/);
- if( res.length !== globs.length ){
- ts.toss(argv[0], " failure: input has ", res.length,
- " row(s) but expecting ",globs.length);
- }
- for(let i = 0; i < res.length; ++i){
- const glob = globs[i].replaceAll(/\s+/g," ").trim();
- //ts.verbose2(argv[0]," <<",glob,">> vs <<",res[i],">>");
- if( this.#jsonMode ){
- if( glob!==res[i] ){
- ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
- res[i],">>");
- }
- }else if( 0!=Util.strglob(glob, res[i]) ){
- ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
- }
- }
- }
- }
- //! --json-block command
- class JsonBlockCommand extends TableResultCommand {
- constructor(){ super(true); }
- }
- //! --testcase command
- class TestCaseCommand extends Command {
- process(tester, script, argv){
- this.argcCheck(script, argv,1);
- script.testCaseName(argv[1]);
- tester.clearResultBuffer();
- tester.clearInputBuffer();
- }
- }
- //! --verbosity command
- class VerbosityCommand extends Command {
- process(t, ts, argv){
- this.argcCheck(ts,argv,1);
- ts.verbosity( parseInt(argv[1]) );
- }
- }
- class CommandDispatcher {
- static map = newObj();
- static getCommandByName(name){
- let rv = CommandDispatcher.map[name];
- if( rv ) return rv;
- switch(name){
- case "close": rv = new CloseDbCommand(); break;
- case "column-names": rv = new ColumnNamesCommand(); break;
- case "db": rv = new DbCommand(); break;
- case "glob": rv = new GlobCommand(); break;
- case "json": rv = new JsonCommand(); break;
- case "json-block": rv = new JsonBlockCommand(); break;
- case "new": rv = new NewDbCommand(); break;
- case "notglob": rv = new NotGlobCommand(); break;
- case "null": rv = new NullCommand(); break;
- case "oom": rv = new NoopCommand(); break;
- case "open": rv = new OpenDbCommand(); break;
- case "print": rv = new PrintCommand(); break;
- case "result": rv = new ResultCommand(); break;
- case "run": rv = new RunCommand(); break;
- case "tableresult": rv = new TableResultCommand(); break;
- case "testcase": rv = new TestCaseCommand(); break;
- case "verbosity": rv = new VerbosityCommand(); break;
- }
- if( rv ){
- CommandDispatcher.map[name] = rv;
- }
- return rv;
- }
- static dispatch(tester, testScript, argv){
- const cmd = CommandDispatcher.getCommandByName(argv[0]);
- if( !cmd ){
- toss(UnknownCommand,testScript,argv[0]);
- }
- cmd.process(tester, testScript, argv);
- }
- }/*CommandDispatcher*/
- const namespace = newObj({
- Command,
- DbException,
- IncompatibleDirective,
- Outer,
- SQLTester,
- SQLTesterException,
- TestScript,
- TestScriptFailed,
- UnknownCommand,
- Util,
- sqlite3
- });
- export {namespace as default};
|