mirror of
https://github.com/sqlite/sqlite.git
synced 2025-04-21 19:26:38 +03:00
513 lines
12 KiB
JavaScript
513 lines
12 KiB
JavaScript
/*
|
|
** 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.
|
|
*/
|
|
|
|
// UNDER CONSTRUCTION. Still being ported from the Java impl.
|
|
|
|
import sqlite3ApiInit from '/jswasm/sqlite3.mjs';
|
|
|
|
const sqlite3 = await sqlite3ApiInit();
|
|
|
|
const log = (...args)=>{
|
|
console.log('SQLTester:',...args);
|
|
};
|
|
|
|
// 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(...args){
|
|
super(args.join(''));
|
|
}
|
|
isFatal() { return false; }
|
|
}
|
|
|
|
SQLTesterException.toss = (...args)=>{
|
|
throw new SQLTesterException(...args);
|
|
}
|
|
|
|
class DbException extends SQLTesterException {
|
|
constructor(...args){
|
|
super(...args);
|
|
//TODO...
|
|
//const db = args[0];
|
|
//if( db instanceof sqlite3.oo1.DB )
|
|
}
|
|
isFatal() { return true; }
|
|
}
|
|
|
|
class TestScriptFailed extends SQLTesterException {
|
|
constructor(testScript, ...args){
|
|
super(testScript.getPutputPrefix(),': ',...args);
|
|
}
|
|
isFatal() { return true; }
|
|
}
|
|
|
|
class UnknownCommand extends SQLTesterException {
|
|
constructor(...args){
|
|
super(...args);
|
|
}
|
|
}
|
|
|
|
class IncompatibleDirective extends SQLTesterException {
|
|
constructor(...args){
|
|
super(...args);
|
|
}
|
|
}
|
|
|
|
const toss = (errType, ...args)=>{
|
|
if( !(errType instanceof SQLTesterException)){
|
|
args.unshift(errType);
|
|
errType = SQLTesterException;
|
|
}
|
|
throw new errType(...args);
|
|
};
|
|
|
|
const __utf8Decoder = new TextDecoder();
|
|
const __utf8Encoder = new TextEncoder('utf-8');
|
|
const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer)
|
|
? function(){} : globalThis.SharedArrayBuffer;
|
|
|
|
const Util = newObj({
|
|
toss,
|
|
|
|
unlink: function(fn){
|
|
return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn);
|
|
},
|
|
|
|
argvToString: (list)=>list.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)
|
|
})/*Util*/;
|
|
|
|
class Outer {
|
|
#lnBuf = [];
|
|
#verbosity = 0;
|
|
#logger = console.log.bind(console);
|
|
|
|
constructor(){
|
|
}
|
|
|
|
out(...args){
|
|
if(!this.#lnBuf.length && this.getOutputPrefix ){
|
|
this.#lnBuf.push(this.getOutputPrefix());
|
|
}
|
|
this.#lnBuf.push(...args);
|
|
return this;
|
|
}
|
|
outln(...args){
|
|
if(!this.#lnBuf.length && this.getOutputPrefix ){
|
|
this.#lnBuf.push(this.getOutputPrefix());
|
|
}
|
|
this.#lnBuf.push(...args,'\n');
|
|
this.#logger(this.#lnBuf.join(''));
|
|
this.#lnBuf.length = 0;
|
|
return this;
|
|
}
|
|
|
|
setOutputPrefix( func ){
|
|
this.getOutputPrefix = func;
|
|
return this;
|
|
}
|
|
|
|
verboseN(lvl, argv){
|
|
if( this.#verbosity>=lvl ){
|
|
this.outln('VERBOSE ',lvl,': ',...argv);
|
|
}
|
|
}
|
|
verbose1(...args){ return this.verboseN(1,args); }
|
|
verbose2(...args){ return this.verboseN(2,args); }
|
|
verbose3(...args){ return this.verboseN(3,args); }
|
|
|
|
verbosity(){
|
|
let rc;
|
|
if(arguments.length){
|
|
rc = this.#verbosity;
|
|
this.#verbosity = arguments[0];
|
|
}else{
|
|
rc = this.#verbosity;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
}/*Outer*/
|
|
|
|
class SQLTester {
|
|
|
|
#outer = new Outer().setOutputPrefix( ()=>'SQLTester: ' );
|
|
#aFiles = [];
|
|
#inputBuffer = [];
|
|
#resultBuffer = [];
|
|
#nullView = "nil";
|
|
#metrics = newObj({
|
|
nTotalTest: 0, nTestFile: 0, nAbortedScript: 0
|
|
});
|
|
#emitColNames = false;
|
|
#keepGoing = false;
|
|
#aDb = [];
|
|
#db = newObj({
|
|
list: [],
|
|
iCurrent: 0,
|
|
initialDbName: "test.db",
|
|
});
|
|
|
|
constructor(){
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
clearInputBuffer(){
|
|
this.#inputBuffer.length = 0;
|
|
return this.#inputBuffer;
|
|
}
|
|
clearResultBuffer(){
|
|
this.#resultBuffer.length = 0;
|
|
return this.#resultBuffer;
|
|
}
|
|
|
|
getInputText(){ return this.#inputBuffer.join(''); }
|
|
getResultText(){ return this.#resultBuffer.join(''); }
|
|
|
|
verbosity(...args){ return this.#outer.verbosity(...args); }
|
|
|
|
}/*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 TestCase extends Command {
|
|
|
|
process(tester, script, argv){
|
|
this.argcCheck(script, argv,1);
|
|
script.testCaseName(argv[1]);
|
|
tester.clearResultBuffer();
|
|
tester.clearInputBuffer();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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-]+)( .*)?)$/
|
|
});
|
|
|
|
class TestScript {
|
|
#cursor = new Cursor();
|
|
#moduleName = null;
|
|
#filename = null;
|
|
#testCaseName = null;
|
|
#outer = new Outer().setOutputPrefix( ()=>this.getOutputPrefix() );
|
|
|
|
constructor(...args){
|
|
let content, filename;
|
|
if( 2 == args.length ){
|
|
filename = args[0];
|
|
content = args[1];
|
|
}else{
|
|
content = args[0];
|
|
}
|
|
this.#filename = filename;
|
|
this.#cursor.src = content;
|
|
this.#outer.outputPrefix = ()=>this.getOutputPrefix();
|
|
}
|
|
|
|
testCaseName(){
|
|
return (0==arguments.length)
|
|
? this.#testCaseName : (this.#testCaseName = arguments[0]);
|
|
}
|
|
|
|
getOutputPrefix() {
|
|
let rc = "["+(this.#moduleName || this.#filename)+"]";
|
|
if( this.#testCaseName ) rc += "["+this.#testCaseName+"]";
|
|
return rc + " line "+ this.#cursor.lineNo +" ";
|
|
}
|
|
|
|
reset(){
|
|
this.#testCaseName = null;
|
|
this.#cursor.rewind();
|
|
return this;
|
|
}
|
|
|
|
toss(...args){
|
|
throw new TestScriptFailed(this,...args);
|
|
}
|
|
|
|
#checkForDirective(tester,line){
|
|
//todo
|
|
}
|
|
|
|
#getCommandArgv(line){
|
|
const m = Rx.command.exec(line);
|
|
return m ? m[1].trim().split(/\s+/) : null;
|
|
}
|
|
|
|
run(tester){
|
|
this.reset();
|
|
this.#outer.verbosity(tester.verbosity());
|
|
let line, directive, argv = [];
|
|
while( null != (line = this.getLine()) ){
|
|
this.verbose3("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.verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
|
|
if(this.#outer.verbosity()>1){
|
|
const input = tester.getInputText();
|
|
if( !!input ) this.verbose3("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;
|
|
const rc = this.getLine();
|
|
cur.peekedPos = cur.pos;
|
|
cur.peekedLineNo = cur.lineNo;
|
|
cur.pos = oldPos;
|
|
cur.lineNo = oldLine;
|
|
cur.putbackPos = oldPB;
|
|
cur.putbackLineNo = oldPBL;
|
|
return rc;
|
|
}
|
|
|
|
|
|
/**
|
|
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;
|
|
}
|
|
|
|
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){ return this.#outer.verbosity(...args); }
|
|
|
|
}/*TestScript*/;
|
|
|
|
class CommandDispatcher {
|
|
static map = newObj();
|
|
|
|
static getCommandByName(name){
|
|
let rv = CommandDispatcher.map[name];
|
|
if( rv ) return rv;
|
|
switch(name){
|
|
//todo: map name to Command instance
|
|
case "testcase": rv = new TestCase(); break;
|
|
}
|
|
if( rv ){
|
|
CommandDispatcher.map[name] = rv;
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
static dispatch(tester, testScript, argv){
|
|
const cmd = CommandDispatcher.getCommandByName(argv[0]);
|
|
if( !cmd ){
|
|
toss(UnknownCommand,argv[0],' ',testScript.getOutputPrefix());
|
|
}
|
|
cmd.process(tester, testScript, argv);
|
|
}
|
|
}/*CommandDispatcher*/
|
|
|
|
const namespace = newObj({
|
|
Command,
|
|
DbException,
|
|
IncompatibleDirective,
|
|
Outer,
|
|
SQLTester,
|
|
SQLTesterException,
|
|
TestScript,
|
|
TestScriptFailed,
|
|
UnknownCommand,
|
|
Util
|
|
});
|
|
|
|
export {namespace as default};
|