1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-30 19:03:16 +03:00

JS SQLTestRunner can now run the Java impl's core-most sanity tests, missing only support for directives.

FossilOrigin-Name: 5e798369375ce1b0c9cdf831f835d931fbd562ff7b4db09a06d1bdca2ac1b975
This commit is contained in:
stephan
2023-08-29 20:01:01 +00:00
parent 69a55ca17d
commit aa15047796
7 changed files with 503 additions and 79 deletions

View File

@ -73,7 +73,7 @@ class DbException extends SQLTesterException {
class TestScriptFailed extends SQLTesterException {
constructor(testScript, ...args){
super(testScript.getPutputPrefix(),': ',...args);
super(testScript.getOutputPrefix(),': ',...args);
}
isFatal() { return true; }
}
@ -103,6 +103,18 @@ const __utf8Encoder = new TextEncoder('utf-8');
const __SAB = ('undefined'===typeof globalThis.SharedArrayBuffer)
? function(){} : globalThis.SharedArrayBuffer;
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]/,
//! Either of '{' or '}'.
squiggly: /[{}]/
});
const Util = newObj({
toss,
@ -110,7 +122,11 @@ const Util = newObj({
return 0==sqlite3.wasm.sqlite3_wasm_vfs_unlink(0,fn);
},
argvToString: (list)=>list.join(" "),
argvToString: (list)=>{
const m = [...list];
m.shift();
return m.join(" ")
},
utf8Decode: function(arrayBuffer, begin, end){
return __utf8Decoder.decode(
@ -120,7 +136,10 @@ const Util = newObj({
);
},
utf8Encode: (str)=>__utf8Encoder.encode(str)
utf8Encode: (str)=>__utf8Encoder.encode(str),
strglob: sqlite3.wasm.xWrap('sqlite3_wasm_SQLTester_strglob','int',
['string','string'])
})/*Util*/;
class Outer {
@ -182,21 +201,39 @@ class Outer {
class SQLTester {
//! Console output utility.
#outer = new Outer().outputPrefix( ()=>'SQLTester: ' );
//! List of input script files.
#aFiles = [];
//! Test input buffer.
#inputBuffer = [];
//! Test result buffer.
#resultBuffer = [];
//! Output representation of SQL NULL.
#nullView = "nil";
#metrics = newObj({
nTotalTest: 0, nTestFile: 0, nAbortedScript: 0
metrics = newObj({
//! Total tests run
nTotalTest: 0,
//! Total test script files run
nTestFile: 0,
//! Number of scripts which were aborted
nAbortedScript: 0,
//! Incremented by test case handlers
nTest: 0
});
#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];
}
@ -208,12 +245,17 @@ class SQLTester {
outln(...args){ return this.#outer.outln(...args); }
out(...args){ return this.#outer.out(...args); }
incrementTestCounter(){
++this.metrics.nTotalTest;
++this.metrics.nTest;
}
reset(){
this.clearInputBuffer();
this.clearResultBuffer();
this.#clearBuffer(this.#db.initSql);
this.closeAllDbs();
this.nTest = 0;
this.metrics.nTest = 0;
this.nullView = "nil";
this.emitColNames = false;
this.#db.iCurrentDb = 0;
@ -365,7 +407,7 @@ class SQLTester {
Util.unlink(this.#db.initialDbName);
this.openDb(0, this.#db.initialDbName, true);
}else{
this.#outer.outln("WARNING: setupInitialDb() unexpectedly ",
this.#outer.outln("WARNING: setupInitialDb() was unexpectedly ",
"triggered while it is opened.");
}
}
@ -405,17 +447,107 @@ class SQLTester {
#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) ){
if( '{' === msg.charAt(0) ){
sb.push(msg);
}else{
sb.push('{', msg, '}');
}
}
#checkDbRc(pDb,rc){
sqlite3.oo1.DB.checkRc(pDb, rc);
}
execSql(pDb, throwOnError, appendMode, lineMode, sql){
sql = sqlite3.capi.sqlite3_js_sql_to_string(sql);
this.#outer.outln("execSql() is TODO. ",sql);
return 0;
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
: new TextEncoder("utf-8").encode(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(pDb, rc);
}else if( sb ){
self.#appendDbErr(db, 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 */
}/* row loop */
if( ResultRowMode.NEWLINE === lineMode ){
spacing = 0;
sb.push('\n');
}
}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(db, sb, rc);
}
break;
}
}/* SQL script loop */;
})/*scopedAllocCall()*/;
return rc;
}
}/*SQLTester*/
@ -469,17 +601,6 @@ class Cursor {
}
}
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]/,
//! Either of '{' or '}'.
squiggly: /[{}]/
});
class TestScript {
#cursor = new Cursor();
#moduleName = null;
@ -529,6 +650,28 @@ class TestScript {
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());
@ -621,14 +764,16 @@ class TestScript {
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;
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;
}
}
@ -667,7 +812,7 @@ class CloseDbCommand extends Command {
let id;
if(argv.length>1){
const arg = argv[1];
if("all".equals(arg)){
if( "all" === arg ){
t.closeAllDbs();
return;
}
@ -697,6 +842,36 @@ class DbCommand extends Command {
}
}
//! --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;
@ -740,6 +915,107 @@ class PrintCommand extends Command {
}
}
//! --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.verbose1(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);
log("TRC fetchCommandBody: ",body);
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);
log("TRC fetchCommandBody reshaped:",body);
}
const globs = body.split(/\s*\n\s*/);
if( globs.length < 1 ){
ts.toss(argv[0], " requires 1 or more ",
(this.#jsonMode ? "json snippets" : "globs"),".");
}
log("TRC fetchCommandBody globs:",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/);
log("TRC fetchCommandBody rbuf, res:",rbuf, res);
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 {
@ -770,18 +1046,18 @@ class CommandDispatcher {
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 "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 "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 "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;
}

View File

@ -25,14 +25,46 @@ log("ns =",ns);
out("Hi there. ").outln("SQLTester is ostensibly ready.");
let ts = new ns.TestScript('/foo.test', ns.Util.utf8Encode(
`# comment line
--print Starting up...
--null NIL
--new :memory:
--testcase 0.0.1
select '0.0.1';
#--result 0.0.1
--print done
`
--close all
--oom
--db 0
--new my.db
--null zilch
--testcase 1.0
SELECT 1, null;
--result 1 zilch
--glob *zil*
--notglob *ZIL*
SELECT 1, 2;
intentional error;
--run
--testcase json-1
SELECT json_array(1,2,3)
--json [1,2,3]
--testcase tableresult-1
select 1, 'a';
select 2, 'b';
--tableresult
# [a-z]
2 b
--end
--testcase json-block-1
select json_array(1,2,3);
select json_object('a',1,'b',2);
--json-block
[1,2,3]
{"a":1,"b":2}
--end
--testcase col-names-on
--column-names 1
select 1 as 'a', 2 as 'b';
--result a 1 b 2
--testcase col-names-off
--column-names 0
select 1 as 'a', 2 as 'b';
--result 1 2
--close
`));
const sqt = new ns.SQLTester();
@ -41,11 +73,12 @@ try{
sqt.openDb('/foo.db', true);
log( 'sqt.getCurrentDb()', sqt.getCurrentDb() );
sqt.verbosity(0);
affirm( 'NIL' !== sqt.nullValue() );
affirm( 'zilch' !== sqt.nullValue() );
ts.run(sqt);
affirm( 'NIL' === sqt.nullValue() );
affirm( 'zilch' === sqt.nullValue() );
}finally{
sqt.reset();
}
log( 'sqt.getCurrentDb()', sqt.getCurrentDb() );
log( "Metrics:", sqt.metrics );

View File

@ -1139,15 +1139,15 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap(
/**
Records the current pstack position, calls the given function,
and restores the pstack regardless of whether the function
throws. Returns the result of the call or propagates an
exception on error.
passing it the sqlite3 object, then restores the pstack
regardless of whether the function throws. Returns the result
of the call or propagates an exception on error.
Added in 3.44.
*/
call: function(f){
const stackPos = wasm.pstack.pointer;
try{ return f() }finally{
try{ return f(sqlite3) } finally{
wasm.pstack.restore(stackPos);
}
}

View File

@ -1801,6 +1801,118 @@ char * sqlite3_wasm_test_str_hello(int fail){
}
return s;
}
/*
** For testing using SQLTester scripts.
**
** Return non-zero if string z matches glob pattern zGlob and zero if the
** pattern does not match.
**
** To repeat:
**
** zero == no match
** non-zero == match
**
** Globbing rules:
**
** '*' Matches any sequence of zero or more characters.
**
** '?' Matches exactly one character.
**
** [...] Matches one character from the enclosed list of
** characters.
**
** [^...] Matches one character not in the enclosed list.
**
** '#' Matches any sequence of one or more digits with an
** optional + or - sign in front, or a hexadecimal
** literal of the form 0x...
*/
static int sqlite3_wasm_SQLTester_strnotglob(const char *zGlob, const char *z){
int c, c2;
int invert;
int seen;
typedef int (*recurse_f)(const char *,const char *);
static const recurse_f recurse = sqlite3_wasm_SQLTester_strnotglob;
while( (c = (*(zGlob++)))!=0 ){
if( c=='*' ){
while( (c=(*(zGlob++))) == '*' || c=='?' ){
if( c=='?' && (*(z++))==0 ) return 0;
}
if( c==0 ){
return 1;
}else if( c=='[' ){
while( *z && recurse(zGlob-1,z)==0 ){
z++;
}
return (*z)!=0;
}
while( (c2 = (*(z++)))!=0 ){
while( c2!=c ){
c2 = *(z++);
if( c2==0 ) return 0;
}
if( recurse(zGlob,z) ) return 1;
}
return 0;
}else if( c=='?' ){
if( (*(z++))==0 ) return 0;
}else if( c=='[' ){
int prior_c = 0;
seen = 0;
invert = 0;
c = *(z++);
if( c==0 ) return 0;
c2 = *(zGlob++);
if( c2=='^' ){
invert = 1;
c2 = *(zGlob++);
}
if( c2==']' ){
if( c==']' ) seen = 1;
c2 = *(zGlob++);
}
while( c2 && c2!=']' ){
if( c2=='-' && zGlob[0]!=']' && zGlob[0]!=0 && prior_c>0 ){
c2 = *(zGlob++);
if( c>=prior_c && c<=c2 ) seen = 1;
prior_c = 0;
}else{
if( c==c2 ){
seen = 1;
}
prior_c = c2;
}
c2 = *(zGlob++);
}
if( c2==0 || (seen ^ invert)==0 ) return 0;
}else if( c=='#' ){
if( z[0]=='0'
&& (z[1]=='x' || z[1]=='X')
&& sqlite3Isxdigit(z[2])
){
z += 3;
while( sqlite3Isxdigit(z[0]) ){ z++; }
}else{
if( (z[0]=='-' || z[0]=='+') && sqlite3Isdigit(z[1]) ) z++;
if( !sqlite3Isdigit(z[0]) ) return 0;
z++;
while( sqlite3Isdigit(z[0]) ){ z++; }
}
}else{
if( c!=(*(z++)) ) return 0;
}
}
return *z==0;
}
SQLITE_WASM_EXPORT
int sqlite3_wasm_SQLTester_strglob(const char *zGlob, const char *z){
return !sqlite3_wasm_SQLTester_strnotglob(zGlob, z);
}
#endif /* SQLITE_WASM_TESTS */
#undef SQLITE_WASM_EXPORT