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

Replace the SQLTester infrastructure with a line-oriented, non-regex-heavy parser. Add --column-names command.

FossilOrigin-Name: 88863908ee2059c2d18a095cbd91f41674c7b0d0a8864ec21715a5317054df4d
This commit is contained in:
stephan
2023-08-10 00:34:38 +00:00
parent e35a703b76
commit 84c1a7b49f
9 changed files with 743 additions and 1439 deletions

View File

@ -21,19 +21,6 @@ import java.util.regex.*;
import org.sqlite.jni.*;
import static org.sqlite.jni.SQLite3Jni.*;
class TestFailure extends RuntimeException {
public TestFailure(String msg){
super(msg);
}
}
class SkipTestRemainder extends RuntimeException {
public TestScript testScript;
public SkipTestRemainder(TestScript ts){
super("Skipping remainder of "+ts.getName());
testScript = ts;
}
}
/**
Modes for how to handle SQLTester.execSql()'s
@ -55,6 +42,12 @@ enum ResultRowMode {
NEWLINE
};
class SQLTesterException extends RuntimeException {
public SQLTesterException(String msg){
super(msg);
}
}
/**
This class provides an application which aims to implement the
rudimentary SQL-driven test tool described in the accompanying
@ -76,6 +69,7 @@ public class SQLTester {
private int nTestFile = 0;
private int nAbortedScript = 0;
private int nTest;
private boolean emitColNames;
private final sqlite3[] aDb = new sqlite3[7];
private int iCurrentDb = 0;
private final String initialDbName = "test.db";
@ -95,6 +89,8 @@ public class SQLTester {
return this.outer.isVerbose();
}
void outputColumnNames(boolean b){ emitColNames = b; }
@SuppressWarnings("unchecked")
public void verbose(Object... vals){
outer.verbose(vals);
@ -125,56 +121,29 @@ public class SQLTester {
return currentScript;
}
public void runTests() throws Exception {
// process each input file
try {
for(String f : listInFiles){
reset();
setupInitialDb();
++nTestFile;
final TestScript ts = new TestScript(f);
currentScript = ts;
outln("----->>>>> ",ts.getModuleName()," [",ts.getName(),"]");
if( ts.isIgnored() ){
outln("WARNING: skipping [",ts.getModuleName(),"]: ",
ts.getIgnoredReason());
continue;
}else{
try{
ts.run(this);
}catch(SkipTestRemainder e){
/* not an error */
++nAbortedScript;
}
}
outln("<<<<<----- ",ts.getModuleName(),": ",nTest," test(s)");
}
}finally{
currentScript = null;
}
Util.unlink(initialDbName);
}
//! Not yet funcional
private void runTests2() throws Exception {
private void runTests() throws Exception {
for(String f : listInFiles){
reset();
setupInitialDb();
++nTestFile;
final TestScript2 ts = new TestScript2(f);
final TestScript ts = new TestScript(f);
outln("----->>>>> running [",f,"]");
try{
ts.run(this);
}catch(SkipTestRemainder2 e){
/* not fatal */
}catch(UnknownCommand e){
/* currently not fatal */
outln(e);
++nAbortedScript;
}catch(IncompatibleDirective e){
/* not fatal */
outln(e);
++nAbortedScript;
}catch(Exception e){
++nAbortedScript;
throw e;
}finally{
outln("<<<<<----- ",nTest," test(s) in ",ts.getFilename());
}
outln("<<<<<----- ",nTest," test(s) in ",ts.getFilename());
}
Util.unlink(initialDbName);
}
@ -264,7 +233,7 @@ public class SQLTester {
if( 0!=rc ){
final String msg = sqlite3_errmsg(db);
sqlite3_close(db);
Util.toss(TestFailure.class, "db open failed with code ",
Util.toss(SQLTesterException.class, "db open failed with code ",
rc," and message: ",msg);
}
return aDb[iCurrentDb] = db;
@ -364,70 +333,89 @@ public class SQLTester {
final StringBuilder sb = (ResultBufferMode.NONE==appendMode)
? null : resultBuffer;
//outln("sqlChunk len= = ",sqlChunk.length);
while(pos < sqlChunk.length){
if(pos > 0){
sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
sqlChunk.length);
}
if( 0==sqlChunk.length ) break;
rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
/*outln("PREPARE rc ",rc," oTail=",oTail.getValue(),": ",
new String(sqlChunk,StandardCharsets.UTF_8),"\n<EOSQL>");*/
if( 0!=rc ){
if(throwOnError){
Util.toss(RuntimeException.class, "db op failed with rc="
+rc+": "+sqlite3_errmsg(db));
}else if( null!=sb ){
appendDbErr(db, sb, rc);
try{
while(pos < sqlChunk.length){
if(pos > 0){
sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
sqlChunk.length);
}
break;
}
pos = oTail.getValue();
stmt = outStmt.getValue();
if( null == stmt ){
// empty statement was parsed.
continue;
}
if( null!=sb ){
// Add the output to the result buffer...
final int nCol = sqlite3_column_count(stmt);
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
for(int i = 0; i < nCol; ++i){
if( spacing++ > 0 ) sb.append(' ');
String val = sqlite3_column_text16(stmt, i);
if( null==val ){
sb.append( nullView );
continue;
}
switch(appendMode){
case ASIS:
sb.append( val );
break;
case ESCAPED:
sb.append( escapeSqlValue(val) );
break;
default:
Util.toss(RuntimeException.class, "Unhandled ResultBufferMode.");
}
}
if( ResultRowMode.NEWLINE == lineMode ){
spacing = 0;
sb.append('\n');
if( 0==sqlChunk.length ) break;
rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
/*outln("PREPARE rc ",rc," oTail=",oTail.getValue(),": ",
new String(sqlChunk,StandardCharsets.UTF_8),"\n<EOSQL>");*/
if( 0!=rc ){
if(throwOnError){
Util.toss(RuntimeException.class, "db op failed with rc="
+rc+": "+sqlite3_errmsg(db));
}else if( null!=sb ){
appendDbErr(db, sb, rc);
}
break;
}
pos = oTail.getValue();
stmt = outStmt.getValue();
if( null == stmt ){
// empty statement was parsed.
continue;
}
}else{
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){}
}
sqlite3_finalize(stmt);
if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
else if( rc!=0 ){
if( null!=sb ){
appendDbErr(db, sb, rc);
// Add the output to the result buffer...
final int nCol = sqlite3_column_count(stmt);
String colName = null, val = null;
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
for(int i = 0; i < nCol; ++i){
if( spacing++ > 0 ) sb.append(' ');
if( emitColNames ){
colName = sqlite3_column_name(stmt, i);
switch(appendMode){
case ASIS:
sb.append( colName );
break;
case ESCAPED:
sb.append( escapeSqlValue(colName) );
break;
default:
Util.toss(RuntimeException.class, "Unhandled ResultBufferMode.");
}
sb.append(' ');
}
val = sqlite3_column_text16(stmt, i);
if( null==val ){
sb.append( nullView );
continue;
}
switch(appendMode){
case ASIS:
sb.append( val );
break;
case ESCAPED:
sb.append( escapeSqlValue(val) );
break;
default:
Util.toss(RuntimeException.class, "Unhandled ResultBufferMode.");
}
}
if( ResultRowMode.NEWLINE == lineMode ){
spacing = 0;
sb.append('\n');
}
}
}else{
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){}
}
sqlite3_finalize(stmt);
stmt = null;
if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
else if( rc!=0 ){
if( null!=sb ){
appendDbErr(db, sb, rc);
}
break;
}
break;
}
}finally{
sqlite3_finalize(stmt);
}
sqlite3_finalize(stmt);
if( 0!=rc && throwOnError ){
Util.toss(RuntimeException.class, "db op failed with rc="
+rc+": "+sqlite3_errmsg(db));
@ -443,8 +431,6 @@ public class SQLTester {
final String flag = a.replaceFirst("-+","");
if( flag.equals("verbose") ){
t.setVerbosity(t.getVerbosity() + 1);
}else if( flag.equals("2") ){
v2 = true;
}else{
throw new IllegalArgumentException("Unhandled flag: "+flag);
}
@ -452,11 +438,13 @@ public class SQLTester {
}
t.addTestScript(a);
}
if( v2 ) t.runTests2();
else t.runTests();
t.outln("Processed ",t.nTotalTest," test(s) in ",t.nTestFile," file(s).");
if( t.nAbortedScript > 0 ){
t.outln("Aborted ",t.nAbortedScript," script(s).");
try {
t.runTests();
}finally{
t.outln("Processed ",t.nTotalTest," test(s) in ",t.nTestFile," file(s).");
if( t.nAbortedScript > 0 ){
t.outln("Aborted ",t.nAbortedScript," script(s).");
}
}
}
@ -492,363 +480,6 @@ public class SQLTester {
}
/**
Base class for test script commands. It provides a set of utility
APIs for concrete command implementations.
Each subclass must have a public no-arg ctor and must implement
the process() method which is abstract in this class.
Commands are intended to be stateless, except perhaps for counters
and similar internals. No state which changes the behavior between
any two invocations of process() should be retained.
*/
abstract class Command {
protected Command(){}
/**
Must process one command-unit of work and either return
(on success) or throw (on error).
The first argument is the context of the test.
argv is a list with the command name followed by any arguments to
that command. The argcCheck() method from this class provides
very basic argc validation.
The content is any text content which was specified after the
command, or null if there is null. Any command which does not
permit content must pass that argument to affirmNoContent() in
their constructor (or perform an equivalent check). Similary,
those which require content must pass it to affirmHasContent()
(or equivalent).
*/
public abstract void process(SQLTester tester, String[] argv, String content) throws Exception;
/**
If argv.length-1 (-1 because the command's name is in argv[0]) does not
fall in the inclusive range (min,max) then this function throws. Use
a max value of -1 to mean unlimited.
*/
protected final void argcCheck(String[] argv, int min, int max) throws Exception{
int argc = argv.length-1;
if(argc<min || (max>=0 && argc>max)){
if( min==max ){
Util.badArg(argv[0]," requires exactly ",min," argument(s)");
}else if(max>0){
Util.badArg(argv[0]," requires ",min,"-",max," arguments.");
}else{
Util.badArg(argv[0]," requires at least ",min," arguments.");
}
}
}
/**
Equivalent to argcCheck(argv,argc,argc).
*/
protected final void argcCheck(String[] argv, int argc) throws Exception{
argcCheck(argv, argc, argc);
}
//! Throws if content is not null.
protected void affirmNoContent(String content) throws Exception{
if(null != content){
Util.badArg(this.getClass().getName()," does not accept content ",
"but got:\n",content);
}
}
//! Throws if content is null.
protected void affirmHasContent(String content) throws Exception{
if(null == content){
Util.badArg(this.getClass().getName()," requires content.");
}
}
}
class CloseDbCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,0,1);
affirmNoContent(content);
Integer id;
if(argv.length>1){
String arg = argv[1];
if("all".equals(arg)){
//t.verbose(argv[0]," all dbs");
t.closeAllDbs();
return;
}
else{
id = Integer.parseInt(arg);
}
}else{
id = t.getCurrentDbId();
}
t.closeDb(id);
t.verbose(argv[0]," db ",id);
}
}
//! --db command
class DbCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,1);
affirmNoContent(content);
final sqlite3 db = t.setCurrentDb( Integer.parseInt(argv[1]) );
//t.verbose(argv[0]," set db to ",db);
}
}
//! --glob command
class GlobCommand extends Command {
private boolean negate = false;
public GlobCommand(){}
protected GlobCommand(boolean negate){ this.negate = negate; }
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,1);
affirmNoContent(content);
t.incrementTestCounter();
final String sql = t.takeInputBuffer();
//t.verbose(argv[0]," SQL =\n",sql);
int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
ResultRowMode.ONELINE, sql);
final String result = t.getResultText();
final String sArgs = Util.argvToString(argv);
//t.verbose(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
final String glob = argv[1];
rc = SQLTester.strglob(glob, result);
if( (negate && 0==rc) || (!negate && 0!=rc) ){
Util.toss(TestFailure.class, argv[0], " mismatch: ",
glob," vs input: ",result);
}
}
}
//! --json command
class JsonCommand extends ResultCommand {
public JsonCommand(){ super(ResultBufferMode.ASIS); }
}
//! --json-block command
class JsonBlockCommand extends TableResultCommand {
public JsonBlockCommand(){ super(true); }
}
//! --new command
class NewDbCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,1);
affirmNoContent(content);
String fname = argv[1];
Util.unlink(fname);
final sqlite3 db = t.openDb(fname, true);
//t.verbose(argv[0]," db ",db);
}
}
//! Placeholder dummy/no-op commands
class NoopCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
}
}
//! --notglob command
class NotGlobCommand extends GlobCommand {
public NotGlobCommand(){
super(true);
}
}
//! --null command
class NullCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,1);
affirmNoContent(content);
t.setNullValue(argv[1]);
//t.verbose(argv[0]," ",argv[1]);
}
}
//! --open command
class OpenDbCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,1);
affirmNoContent(content);
String fname = argv[1];
final sqlite3 db = t.openDb(fname, false);
//t.verbose(argv[0]," db ",db);
}
}
//! --print command
class PrintCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
if( 1==argv.length && null==content ){
Util.badArg(argv[0]," requires at least 1 argument or body content.");
}
if( argv.length > 1 ) t.outln("\t",Util.argvToString(argv));
if( null!=content ) t.outln(content.replaceAll("(?m)^", "\t"));
}
}
//! --result command
class ResultCommand extends Command {
private final ResultBufferMode bufferMode;
protected ResultCommand(ResultBufferMode bm){ bufferMode = bm; }
public ResultCommand(){ this(ResultBufferMode.ESCAPED); }
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,0,-1);
affirmNoContent(content);
t.incrementTestCounter();
final String sql = t.takeInputBuffer();
//t.verbose(argv[0]," SQL =\n",sql);
int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
final String result = t.getResultText().trim();
final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
if( !result.equals(sArgs) ){
t.outln(argv[0]," FAILED comparison. Result buffer:\n",
result,"\nargs:\n",sArgs);
Util.toss(TestFailure.class, argv[0]," comparison failed.");
}
}
}
//! --run command
class RunCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,0,1);
affirmHasContent(content);
final sqlite3 db = (1==argv.length)
? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
int rc = t.execSql(db, false, ResultBufferMode.NONE,
ResultRowMode.ONELINE, content);
if( 0!=rc && t.isVerbose() ){
String msg = sqlite3_errmsg(db);
t.verbose(argv[0]," non-fatal command error #",rc,": ",
msg,"\nfor SQL:\n",content);
}
}
}
//! --tableresult command
class TableResultCommand extends Command {
private final boolean jsonMode;
protected TableResultCommand(boolean jsonMode){ this.jsonMode = jsonMode; }
public TableResultCommand(){ this(false); }
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,0);
affirmHasContent(content);
t.incrementTestCounter();
if( !content.endsWith("\n--end") ){
Util.toss(TestFailure.class, argv[0], " must be terminated with --end.");
}else{
int n = content.length();
content = content.substring(0, n-6);
}
final String[] globs = content.split("\\s*\\n\\s*");
if( globs.length < 1 ){
Util.toss(TestFailure.class, argv[0], " requires 1 or more ",
(jsonMode ? "json snippets" : "globs"),".");
}
final String sql = t.takeInputBuffer();
t.execSql(null, true,
jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
ResultRowMode.NEWLINE, sql);
final String rbuf = t.getResultText();
final String[] res = rbuf.split("\n");
if( res.length != globs.length ){
Util.toss(TestFailure.class, argv[0], " failure: input has ",
res.length," row(s) but expecting ",globs.length);
}
for(int i = 0; i < res.length; ++i){
final String glob = globs[i].replaceAll("\\s+"," ").trim();
//t.verbose(argv[0]," <<",glob,">> vs <<",res[i],">>");
if( jsonMode ){
if( !glob.equals(res[i]) ){
Util.toss(TestFailure.class, argv[0], " json <<",glob,
">> does not match: <<",res[i],">>");
}
}else if( 0 != SQLTester.strglob(glob, res[i]) ){
Util.toss(TestFailure.class, argv[0], " glob <<",glob,
">> does not match: <<",res[i],">>");
}
}
}
}
//! --testcase command
class TestCaseCommand extends Command {
public void process(SQLTester t, String[] argv, String content) throws Exception{
argcCheck(argv,1);
affirmHasContent(content);
// TODO: do something with the test name
t.clearResultBuffer();
t.clearInputBuffer().append(content);
//t.verbose(argv[0]," input buffer: ",content);
}
}
/**
Helper for dispatching Command instances.
*/
class CommandDispatcher {
private static java.util.Map<String,Command> commandMap =
new java.util.HashMap<>();
/**
Returns a (cached) instance mapped to name, or null if no match
is found.
*/
static Command getCommandByName(String name){
Command rv = commandMap.get(name);
if( null!=rv ) return rv;
switch(name){
case "close": rv = new CloseDbCommand(); 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;
default: rv = null; break;
}
if( null!=rv ) commandMap.put(name, rv);
return rv;
}
/**
Treats argv[0] as a command name, looks it up with
getCommandByName(), and calls process() on that instance, passing
it arguments given to this function.
*/
static void dispatch(SQLTester tester, String[] argv, String content) throws Exception{
final Command cmd = getCommandByName(argv[0]);
if(null == cmd){
final TestScript ts = tester.getCurrentScript();
if( tester.skipUnknownCommands() ){
tester.outln("WARNING: skipping remainder of [",ts.getModuleName(),
"] because it contains unknown command '",argv[0],"'.");
throw new SkipTestRemainder(ts);
}
Util.toss(IllegalArgumentException.class,
"No command handler found for '"+argv[0]+"' in ",
ts.getName());
}
//tester.verbose("Running ",argv[0]," with:\n", content);
cmd.process(tester, argv, content);
}
}
/**
General utilities for the SQLTester bits.
*/

View File

@ -12,29 +12,384 @@
** This file contains the TestScript part of the SQLTester framework.
*/
package org.sqlite.jni.tester;
import java.util.List;
import java.util.ArrayList;
import java.io.*;
import static org.sqlite.jni.SQLite3Jni.*;
import org.sqlite.jni.sqlite3;
import java.util.Arrays;
import java.nio.charset.StandardCharsets;
import java.util.regex.*;
class TestScriptFailed extends SQLTesterException {
public TestScriptFailed(TestScript ts, String msg){
super(ts.getOutputPrefix()+": "+msg);
}
}
class UnknownCommand extends SQLTesterException {
public UnknownCommand(TestScript ts, String cmd){
super(ts.getOutputPrefix()+": unknown command: "+cmd);
}
}
class IncompatibleDirective extends SQLTesterException {
public IncompatibleDirective(TestScript ts, String line){
super(ts.getOutputPrefix()+": incompatible directive: "+line);
}
}
/**
Base class for test script commands. It provides a set of utility
APIs for concrete command implementations.
Each subclass must have a public no-arg ctor and must implement
the process() method which is abstract in this class.
Commands are intended to be stateless, except perhaps for counters
and similar internals. Specifically, no state which changes the
behavior between any two invocations of process() should be
retained.
*/
abstract class Command {
protected Command(){}
/**
Must process one command-unit of work and either return
(on success) or throw (on error).
The first two arguments specify the context of the test.
argv is a list with the command name followed by any arguments to
that command. The argcCheck() method from this class provides
very basic argc validation.
*/
public abstract void process(
SQLTester st, TestScript ts, String[] argv
) throws Exception;
/**
If argv.length-1 (-1 because the command's name is in argv[0]) does not
fall in the inclusive range (min,max) then this function throws. Use
a max value of -1 to mean unlimited.
*/
protected final void argcCheck(TestScript ts, String[] argv, int min, int max) throws Exception{
int argc = argv.length-1;
if(argc<min || (max>=0 && argc>max)){
if( min==max ){
ts.toss(argv[0]," requires exactly ",min," argument(s)");
}else if(max>0){
ts.toss(argv[0]," requires ",min,"-",max," arguments.");
}else{
ts.toss(argv[0]," requires at least ",min," arguments.");
}
}
}
/**
Equivalent to argcCheck(argv,argc,argc).
*/
protected final void argcCheck(TestScript ts, String[] argv, int argc) throws Exception{
argcCheck(ts, argv, argc, argc);
}
}
//! --close command
class CloseDbCommand extends Command {
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,0,1);
Integer id;
if(argv.length>1){
String arg = argv[1];
if("all".equals(arg)){
t.closeAllDbs();
return;
}
else{
id = Integer.parseInt(arg);
}
}else{
id = t.getCurrentDbId();
}
t.closeDb(id);
}
}
//! --column-names command
class ColumnNamesCommand extends Command {
public void process(
SQLTester st, TestScript ts, String[] argv
) throws Exception{
argcCheck(ts,argv,1);
st.outputColumnNames( Integer.parseInt(argv[1])!=0 );
}
}
//! --db command
class DbCommand extends Command {
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,1);
t.setCurrentDb( Integer.parseInt(argv[1]) );
}
}
//! --glob command
class GlobCommand extends Command {
private boolean negate = false;
public GlobCommand(){}
protected GlobCommand(boolean negate){ this.negate = negate; }
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,1);
t.incrementTestCounter();
final String sql = t.takeInputBuffer();
int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
ResultRowMode.ONELINE, sql);
final String result = t.getResultText();
final String sArgs = Util.argvToString(argv);
//t.verbose(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
final String glob = argv[1];
rc = SQLTester.strglob(glob, result);
if( (negate && 0==rc) || (!negate && 0!=rc) ){
ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
}
}
}
//! --json command
class JsonCommand extends ResultCommand {
public JsonCommand(){ super(ResultBufferMode.ASIS); }
}
//! --json-block command
class JsonBlockCommand extends TableResultCommand {
public JsonBlockCommand(){ super(true); }
}
//! --new command
class NewDbCommand extends OpenDbCommand {
public NewDbCommand(){ super(true); }
}
//! Placeholder dummy/no-op commands
class NoopCommand extends Command {
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
}
}
//! --notglob command
class NotGlobCommand extends GlobCommand {
public NotGlobCommand(){
super(true);
}
}
//! --null command
class NullCommand extends Command {
public void process(
SQLTester st, TestScript ts, String[] argv
) throws Exception{
argcCheck(ts,argv,1);
st.setNullValue( argv[1] );
}
}
//! --open command
class OpenDbCommand extends Command {
private boolean createIfNeeded = false;
public OpenDbCommand(){}
protected OpenDbCommand(boolean c){createIfNeeded = c;}
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,1);
t.openDb(argv[1], createIfNeeded);
}
}
//! --print command
class PrintCommand extends Command {
public void process(
SQLTester st, TestScript ts, String[] argv
) throws Exception{
st.out(ts.getOutputPrefix(),": ");
final String body = ts.fetchCommandBody();
if( 1==argv.length && null==body ){
st.out( st.getInputText() );
}else{
st.outln( Util.argvToString(argv) );
}
if( null!=body ){
st.out(body);
}
}
}
//! --result command
class ResultCommand extends Command {
private final ResultBufferMode bufferMode;
protected ResultCommand(ResultBufferMode bm){ bufferMode = bm; }
public ResultCommand(){ this(ResultBufferMode.ESCAPED); }
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,0,-1);
t.incrementTestCounter();
final String sql = t.takeInputBuffer();
//t.verbose(argv[0]," SQL =\n",sql);
int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
final String result = t.getResultText().trim();
final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
if( !result.equals(sArgs) ){
t.outln(argv[0]," FAILED comparison. Result buffer:\n",
result,"\nargs:\n",sArgs);
ts.toss(argv[0]+" comparison failed.");
}
}
}
//! --run command
class RunCommand extends Command {
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,0,1);
final sqlite3 db = (1==argv.length)
? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
final String sql = t.takeInputBuffer();
int rc = t.execSql(db, false, ResultBufferMode.NONE,
ResultRowMode.ONELINE, sql);
if( 0!=rc && t.isVerbose() ){
String msg = sqlite3_errmsg(db);
t.verbose(argv[0]," non-fatal command error #",rc,": ",
msg,"\nfor SQL:\n",sql);
}
}
}
//! --tableresult command
class TableResultCommand extends Command {
private final boolean jsonMode;
protected TableResultCommand(boolean jsonMode){ this.jsonMode = jsonMode; }
public TableResultCommand(){ this(false); }
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,0);
t.incrementTestCounter();
String body = ts.fetchCommandBody();
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.");
}else{
int n = body.length();
body = body.substring(0, n-6);
}
final String[] globs = body.split("\\s*\\n\\s*");
if( globs.length < 1 ){
ts.toss(argv[0], " requires 1 or more ",
(jsonMode ? "json snippets" : "globs"),".");
}
final String sql = t.takeInputBuffer();
t.execSql(null, true,
jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
ResultRowMode.NEWLINE, sql);
final String rbuf = t.getResultText();
final String[] res = rbuf.split("\n");
if( res.length != globs.length ){
ts.toss(argv[0], " failure: input has ", res.length,
" row(s) but expecting ",globs.length);
}
for(int i = 0; i < res.length; ++i){
final String glob = globs[i].replaceAll("\\s+"," ").trim();
//t.verbose(argv[0]," <<",glob,">> vs <<",res[i],">>");
if( jsonMode ){
if( !glob.equals(res[i]) ){
ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
res[i],">>");
}
}else if( 0 != SQLTester.strglob(glob, res[i]) ){
ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
}
}
}
}
//! --testcase command
class TestCaseCommand extends Command {
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
argcCheck(ts,argv,1);
// TODO?: do something with the test name
t.clearResultBuffer();
t.clearInputBuffer();
}
}
class CommandDispatcher2 {
private static java.util.Map<String,Command> commandMap =
new java.util.HashMap<>();
/**
Returns a (cached) instance mapped to name, or null if no match
is found.
*/
static Command getCommandByName(String name){
Command rv = commandMap.get(name);
if( null!=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;
default: rv = null; break;
}
if( null!=rv ) commandMap.put(name, rv);
return rv;
}
/**
Treats argv[0] as a command name, looks it up with
getCommandByName(), and calls process() on that instance, passing
it arguments given to this function.
*/
static void dispatch(SQLTester tester, TestScript ts, String[] argv) throws Exception{
final Command cmd = getCommandByName(argv[0]);
if(null == cmd){
throw new UnknownCommand(ts, argv[0]);
}
cmd.process(tester, ts, argv);
}
}
/**
This class represents a single test script. It handles (or
delegates) its the reading-in and parsing, but the details of
evaluation are delegated elsewhere.
*/
class TestScript {
private String name = null;
private String filename = null;
private String moduleName = null;
private List<CommandChunk> chunks = null;
private final Cursor cur = new Cursor();
private final Outer outer = new Outer();
private String ignoreReason = null;
private byte[] baScript = null;
/* One "chunk" of input, representing a single command and
its optional body content. */
private static final class CommandChunk {
public String[] argv = null;
public String content = null;
private static final class Cursor {
private final StringBuilder sb = new StringBuilder();
byte[] src = null;
int pos = 0;
int putbackPos = 0;
int putbackLineNo = 0;
int lineNo = 0 /* yes, zero */;
int peekedPos = 0;
int peekedLineNo = 0;
boolean inComment = false;
void reset(){
sb.setLength(0); pos = 0; lineNo = 0/*yes, zero*/; inComment = false;
}
}
private byte[] readFile(String filename) throws Exception {
@ -43,229 +398,289 @@ class TestScript {
/**
Initializes the script with the content of the given file.
Throws if it cannot read the file or if tokenizing it fails.
Throws if it cannot read the file.
*/
public TestScript(String filename) throws Exception{
name = filename;
baScript = readFile(filename);
setContent(new String(
baScript, java.nio.charset.StandardCharsets.UTF_8
));
this.filename = filename;
setVerbosity(2);
cur.src = readFile(filename);
}
/**
Initializes the script with the given content, copied at
construction-time. The first argument is a filename for that
content. It need not refer to a real file - it's for display
purposes only.
*/
public TestScript(String virtualName, StringBuffer content)
throws RuntimeException {
name = virtualName;
setContent(content.toString());
}
private void setContent(String c){
this.chunks = chunkContent(c);
}
public String getName(){
return name;
public String getFilename(){
return filename;
}
public String getModuleName(){
return moduleName;
}
public boolean isIgnored(){
return null!=ignoreReason;
}
public String getIgnoredReason(){
return ignoreReason;
}
public void setVerbosity(int level){
outer.setVerbosity(level);
}
public String getOutputPrefix(){
return "["+(moduleName==null ? filename : moduleName)+"] line "+
cur.lineNo;
}
@SuppressWarnings("unchecked")
private TestScript verbose(Object... vals){
outer.verbose(vals);
private TestScript verboseN(int level, Object... vals){
final int verbosity = outer.getVerbosity();
if(verbosity>=level){
outer.out("VERBOSE",(verbosity>1 ? "+ " : " "),
getOutputPrefix(),": ");
outer.outln(vals);
}
return this;
}
private static final Pattern patternHashLine =
Pattern.compile("^#", Pattern.MULTILINE);
private TestScript verbose1(Object... vals){return verboseN(1,vals);}
private TestScript verbose2(Object... vals){return verboseN(2,vals);}
@SuppressWarnings("unchecked")
public TestScript warn(Object... vals){
outer.out("WARNING ", getOutputPrefix(),": ");
outer.outln(vals);
return this;
}
private void reset(){
cur.reset();
}
/**
Returns the next line from the buffer, minus the trailing EOL.
Returns null when all input is consumed. Throws if it reads
illegally-encoded input, e.g. (non-)characters in the range
128-256.
*/
String getLine(){
if( cur.pos==cur.src.length ){
return null /* EOF */;
}
cur.putbackPos = cur.pos;
cur.putbackLineNo = cur.lineNo;
cur.sb.setLength(0);
final boolean skipLeadingWs = false;
byte b = 0, prevB = 0;
int i = cur.pos;
if(skipLeadingWs) {
/* Skip any leading spaces, including newlines. This will eliminate
blank lines. */
for(; i < cur.src.length; ++i, prevB=b){
b = cur.src[i];
switch((int)b){
case 32/*space*/: case 9/*tab*/: case 13/*CR*/: continue;
case 10/*NL*/: ++cur.lineNo; continue;
default: break;
}
break;
}
if( i==cur.src.length ){
return null /* EOF */;
}
}
boolean doBreak = false;
final byte[] aChar = {0,0,0,0} /* multi-byte char buffer */;
int nChar = 0 /* number of bytes in the char */;
for(; i < cur.src.length && !doBreak; ++i){
b = cur.src[i];
switch( (int)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. Appending individual bytes to the StringBuffer
appends their integer value. */
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 (#"+(int)b+").");
break;
}
if( 1==nChar ){
cur.sb.append((char)b);
}else{
for(int x = 0; x < nChar; ++x) aChar[x] = cur.src[i+x];
cur.sb.append(new String(Arrays.copyOf(aChar, nChar),
StandardCharsets.UTF_8));
i += nChar-1;
}
break;
}
}
cur.pos = i;
final String rv = cur.sb.toString();
if( i==cur.src.length && 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.
*/
String peekLine(){
final int oldPos = cur.pos;
final int oldPB = cur.putbackPos;
final int oldPBL = cur.putbackLineNo;
final int oldLine = cur.lineNo;
final String rc = 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().
*/
void consumePeeked(){
cur.pos = cur.peekedPos;
cur.lineNo = cur.peekedLineNo;
}
/**
Restores the cursor to the position it had before the previous
call to getLine().
*/
void putbackLine(){
cur.pos = cur.putbackPos;
cur.lineNo = cur.putbackLineNo;
}
private static final Pattern patternRequiredProperties =
Pattern.compile("REQUIRED_PROPERTIES:[ \\t]*(\\S+\\s*)\\n");
/**
Returns true if the given script content should be ignored
(because it contains certain content which indicates such).
*/
private boolean shouldBeIgnored(String content){
if( null == moduleName ){
ignoreReason = "No module name.";
return true;
}else if( content.indexOf("\n|")>=0 ){
ignoreReason = "Contains newline-pipe combination.";
return true;
}else if( content.indexOf(" MODULE_NAME:")>=0 ){
ignoreReason = "Contains MODULE_NAME.";
return true;
}else if( content.indexOf("MIXED_MODULE_NAME:")>=0 ){
ignoreReason = "Contains MIXED_MODULE_NAME.";
return true;
}
Matcher m = patternHashLine.matcher(content);
if( m.find() ){
ignoreReason = "C-preprocessor line found.";
return true;
}
m = patternRequiredProperties.matcher(content);
if( m.find() ){
ignoreReason = "REQUIRED_PROPERTIES found: "+m.group(1).trim();
return true;
}
return false;
}
private boolean findModuleName(String content){
final Pattern p = Pattern.compile(
"SCRIPT_MODULE_NAME:\\s+(\\S+)\\s*\n",
Pattern.MULTILINE
);
final Matcher m = p.matcher(content);
moduleName = m.find() ? m.group(1) : null;
return moduleName != null;
}
Pattern.compile(" REQUIRED_PROPERTIES:[ \\t]*(\\S.*)\\s*$");
private static final Pattern patternScriptModuleName =
Pattern.compile(" SCRIPT_MODULE_NAME:[ \\t]*(\\S+)\\s*$");
private static final Pattern patternMixedModuleName =
Pattern.compile(" ((MIXED_)?MODULE_NAME):[ \\t]*(\\S+)\\s*$");
private static final Pattern patternCommand =
Pattern.compile("^--(([a-z-]+)( .*)?)$");
/**
Chop script up into chunks containing individual commands and
their inputs. The approach taken here is not as robust as
line-by-line parsing would be but the framework is structured
such that we could replace this part without unduly affecting the
evaluation bits. The potential problems with this approach
include:
- It's potentially possible that it will strip content out of a
testcase block.
- It loses all file location information, so we can't report line
numbers of errors.
If/when that becomes a problem, it can be refactored.
Looks for "directives." If a compatible one is found, it is
processed and this function returns. If an incompatible one is found,
a description of it is returned and processing of the test must
end immediately.
*/
private List<CommandChunk> chunkContent(String content){
findModuleName(content);
if( shouldBeIgnored(content) ){
chunks = null;
return null;
private void checkForDirective(String line) throws IncompatibleDirective {
if(line.startsWith("#")){
throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
}else if(line.startsWith("---")){
new IncompatibleDirective(this, "triple-dash: "+line);
}
Matcher m = patternScriptModuleName.matcher(line);
if( m.find() ){
moduleName = m.group(1);
return;
}
m = patternRequiredProperties.matcher(line);
if( m.find() ){
throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+m.group(1));
}
m = patternMixedModuleName.matcher(line);
if( m.find() ){
throw new IncompatibleDirective(this, m.group(1)+": "+m.group(3));
}
if( line.indexOf("\n|")>=0 ){
throw new IncompatibleDirective(this, "newline-pipe combination.");
}
return;
}
// First, strip out any content which we know we can ignore...
final String sCComment = "[/][*]([*](?![/])|[^*])*[*][/]";
final String s3Dash = "^---+[^\\n]*\\n";
final String sEmptyLine = "^\\n";
final String sOom = "^--oom\\n"
/* Workaround: --oom is a top-level command in some contexts
and appears in --testcase blocks in others. We don't
do anything with --oom commands aside from ignore them, so
elide them all to fix the --testcase blocks which contain
them. */;
final List<String> lPats = new ArrayList<>();
lPats.add(sCComment);
lPats.add(s3Dash);
lPats.add(sEmptyLine);
lPats.add(sOom);
//verbose("Content:").verbose(content).verbose("<EOF>");
for( String s : lPats ){
final Pattern p = Pattern.compile(
s, Pattern.MULTILINE
);
final Matcher m = p.matcher(content);
/*verbose("Pattern {{{ ",p.pattern()," }}} with flags ",
p.flags()," matches:"
);*/
int n = 0;
//while( m.find() ) verbose("#",(++n),"\t",m.group(0).trim());
content = m.replaceAll("");
}
// Chunk the newly-cleaned text into individual commands and their input...
// First split up the input into command-size blocks...
final List<String> blocks = new ArrayList<>();
final Pattern p = Pattern.compile(
"^--(?!end)[a-z]+", Pattern.MULTILINE
// --end is a marker used by --tableresult and --(not)glob.
);
final Matcher m = p.matcher(content);
int ndxPrev = 0, pos = 0, i = 0;
//verbose("Trimmed content:").verbose(content).verbose("<EOF>");
while( m.find() ){
pos = m.start();
final String block = content.substring(ndxPrev, pos).trim();
if( 0==ndxPrev && pos>ndxPrev ){
/* Initial block of non-command state. Skip it. */
ndxPrev = pos + 2;
continue;
}
if( !block.isEmpty() ){
++i;
//verbose("BLOCK #",i," ",+ndxPrev,"..",pos,block);
blocks.add( block );
}
ndxPrev = pos + 2;
}
if( ndxPrev < content.length() ){
// This all belongs to the final command
final String block = content.substring(ndxPrev, content.length()).trim();
if( !block.isEmpty() ){
++i;
//verbose("BLOCK #",(++i)," ",block);
blocks.add( block );
}
}
// Next, convert those blocks into higher-level CommandChunks...
final List<CommandChunk> rc = new ArrayList<>();
for( String block : blocks ){
final CommandChunk chunk = new CommandChunk();
final String[] parts = block.split("\\n", 2);
chunk.argv = parts[0].split("\\s+");
if( parts.length>1 && parts[1].length()>0 ){
chunk.content = parts[1]
/* reminder: don't trim() here. It would be easier
for Command impls if we did but it makes debug
output look weird. */;
}
rc.add( chunk );
boolean isCommandLine(String line, boolean checkForImpl){
final Matcher m = patternCommand.matcher(line);
boolean rc = m.find();
if( rc && checkForImpl ){
rc = null!=CommandDispatcher2.getCommandByName(m.group(2));
}
return rc;
}
/**
Runs this test script in the context of the given tester object.
If line looks like a command, returns an argv for that command
invocation, else returns null.
*/
public void run(SQLTester tester) throws Exception {
final int verbosity = tester.getVerbosity();
if( null==chunks ){
outer.outln("This test contains content which forces it to be skipped.");
}else{
int n = 0;
for(CommandChunk chunk : chunks){
if(verbosity>0){
outer.out("VERBOSE",(verbosity>1 ? "+ " : " "),moduleName,
" #",++n," ",chunk.argv[0],
" ",Util.argvToString(chunk.argv));
if(verbosity>1 && null!=chunk.content){
outer.out("\n", chunk.content);
}
outer.out("\n");
}
CommandDispatcher.dispatch(
tester, chunk.argv,
(null==chunk.content) ? null : chunk.content.trim()
);
String[] getCommandArgv(String line){
final Matcher m = patternCommand.matcher(line);
return m.find() ? m.group(1).trim().split("\\s+") : null;
}
/**
Fetches lines until the next recognized command. Throws if
checkForDirective() does. Returns null if there is no input or
it's only whitespace. The returned string retains all whitespace.
Note that "subcommands", --command-like constructs in the body
which do not match a known command name are considered to be
content, not commands.
*/
String fetchCommandBody(){
final StringBuilder sb = new StringBuilder();
String line;
while( (null != (line = peekLine())) ){
checkForDirective(line);
if( !isCommandLine(line, true) ){
sb.append(line).append("\n");
consumePeeked();
}else{
break;
}
}
line = sb.toString();
return line.trim().isEmpty() ? null : line;
}
private void processCommand(SQLTester t, String[] argv) throws Exception{
verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
if(outer.getVerbosity()>1){
final String input = t.getInputText();
if( !input.isEmpty() ) verbose2("Input buffer = ",input);
}
CommandDispatcher2.dispatch(t, this, argv);
}
void toss(Object... msg) throws TestScriptFailed {
StringBuilder sb = new StringBuilder();
for(Object s : msg) sb.append(s);
throw new TestScriptFailed(this, sb.toString());
}
/**
Runs this test script in the context of the given tester object.
*/
@SuppressWarnings("unchecked")
public boolean run(SQLTester tester) throws Exception {
reset();
setVerbosity(tester.getVerbosity());
String line, directive;
String[] argv;
while( null != (line = getLine()) ){
//verbose(line);
checkForDirective(line);
argv = getCommandArgv(line);
if( null!=argv ){
processCommand(tester, argv);
continue;
}
tester.appendInput(line,true);
}
return true;
}
}

View File

@ -1,680 +0,0 @@
/*
** 2023-08-08
**
** 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 TestScript2 part of the SQLTester framework.
*/
package org.sqlite.jni.tester;
import static org.sqlite.jni.SQLite3Jni.*;
import org.sqlite.jni.sqlite3;
import java.util.Arrays;
import java.nio.charset.StandardCharsets;
import java.util.regex.*;
class SQLTestException extends RuntimeException {
public SQLTestException(String msg){
super(msg);
}
}
class TestScript2Failed extends SQLTestException {
public TestScript2Failed(TestScript2 ts, String msg){
super(ts.getOutputPrefix()+": "+msg);
}
}
class SkipTestRemainder2 extends SQLTestException {
public SkipTestRemainder2(TestScript2 ts){
super(ts.getOutputPrefix()+": skipping remainder");
}
}
class IncompatibleDirective extends SQLTestException {
public IncompatibleDirective(TestScript2 ts, String line){
super(ts.getOutputPrefix()+": incompatible directive: "+line);
}
}
class UnknownCommand extends SQLTestException {
public UnknownCommand(TestScript2 ts, String line){
super(ts.getOutputPrefix()+": unknown command: "+line);
}
}
abstract class Command2 {
protected Command2(){}
public abstract void process(
SQLTester st, TestScript2 ts, String[] argv
) throws Exception;
/**
If argv.length-1 (-1 because the command's name is in argv[0]) does not
fall in the inclusive range (min,max) then this function throws. Use
a max value of -1 to mean unlimited.
*/
protected final void argcCheck(String[] argv, int min, int max) throws Exception{
int argc = argv.length-1;
if(argc<min || (max>=0 && argc>max)){
if( min==max ){
Util.badArg(argv[0]," requires exactly ",min," argument(s)");
}else if(max>0){
Util.badArg(argv[0]," requires ",min,"-",max," arguments.");
}else{
Util.badArg(argv[0]," requires at least ",min," arguments.");
}
}
}
/**
Equivalent to argcCheck(argv,argc,argc).
*/
protected final void argcCheck(String[] argv, int argc) throws Exception{
argcCheck(argv, argc, argc);
}
}
//! --close command
class CloseDbCommand2 extends Command2 {
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,0,1);
Integer id;
if(argv.length>1){
String arg = argv[1];
if("all".equals(arg)){
t.closeAllDbs();
return;
}
else{
id = Integer.parseInt(arg);
}
}else{
id = t.getCurrentDbId();
}
t.closeDb(id);
}
}
//! --db command
class DbCommand2 extends Command2 {
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,1);
t.setCurrentDb( Integer.parseInt(argv[1]) );
}
}
//! --glob command
class GlobCommand2 extends Command2 {
private boolean negate = false;
public GlobCommand2(){}
protected GlobCommand2(boolean negate){ this.negate = negate; }
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,1);
t.incrementTestCounter();
final String sql = t.takeInputBuffer();
int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
ResultRowMode.ONELINE, sql);
final String result = t.getResultText();
final String sArgs = Util.argvToString(argv);
//t.verbose(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
final String glob = argv[1];
rc = SQLTester.strglob(glob, result);
if( (negate && 0==rc) || (!negate && 0!=rc) ){
ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
}
}
}
//! --json command
class JsonCommand2 extends ResultCommand2 {
public JsonCommand2(){ super(ResultBufferMode.ASIS); }
}
//! --json-block command
class JsonBlockCommand2 extends TableResultCommand2 {
public JsonBlockCommand2(){ super(true); }
}
//! --new command
class NewDbCommand2 extends OpenDbCommand2 {
public NewDbCommand2(){ super(true); }
}
//! Placeholder dummy/no-op commands
class NoopCommand2 extends Command2 {
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
}
}
//! --notglob command
class NotGlobCommand2 extends GlobCommand2 {
public NotGlobCommand2(){
super(true);
}
}
//! --null command
class NullCommand2 extends Command2 {
public void process(
SQLTester st, TestScript2 ts, String[] argv
) throws Exception{
argcCheck(argv,1);
st.setNullValue( argv[1] );
}
}
//! --open command
class OpenDbCommand2 extends Command2 {
private boolean createIfNeeded = false;
public OpenDbCommand2(){}
protected OpenDbCommand2(boolean c){createIfNeeded = c;}
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,1);
t.openDb(argv[1], createIfNeeded);
}
}
//! --print command
class PrintCommand2 extends Command2 {
public void process(
SQLTester st, TestScript2 ts, String[] argv
) throws Exception{
st.out(ts.getOutputPrefix(),": ");
final String body = ts.fetchCommandBody();
if( 1==argv.length && null==body ){
st.out( st.getInputText() );
}else{
st.outln( Util.argvToString(argv) );
}
if( null!=body ){
st.out(body);
}
}
}
//! --result command
class ResultCommand2 extends Command2 {
private final ResultBufferMode bufferMode;
protected ResultCommand2(ResultBufferMode bm){ bufferMode = bm; }
public ResultCommand2(){ this(ResultBufferMode.ESCAPED); }
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,0,-1);
t.incrementTestCounter();
final String sql = t.takeInputBuffer();
//t.verbose(argv[0]," SQL =\n",sql);
int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
final String result = t.getResultText().trim();
final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
if( !result.equals(sArgs) ){
t.outln(argv[0]," FAILED comparison. Result buffer:\n",
result,"\nargs:\n",sArgs);
ts.toss(argv[0]+" comparison failed.");
}
}
}
//! --run command
class RunCommand2 extends Command2 {
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,0,1);
final sqlite3 db = (1==argv.length)
? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
final String sql = t.takeInputBuffer();
int rc = t.execSql(db, false, ResultBufferMode.NONE,
ResultRowMode.ONELINE, sql);
if( 0!=rc && t.isVerbose() ){
String msg = sqlite3_errmsg(db);
t.verbose(argv[0]," non-fatal command error #",rc,": ",
msg,"\nfor SQL:\n",sql);
}
}
}
//! --tableresult command
class TableResultCommand2 extends Command2 {
private final boolean jsonMode;
protected TableResultCommand2(boolean jsonMode){ this.jsonMode = jsonMode; }
public TableResultCommand2(){ this(false); }
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,0);
t.incrementTestCounter();
String body = ts.fetchCommandBody();
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.");
}else{
int n = body.length();
body = body.substring(0, n-6);
}
final String[] globs = body.split("\\s*\\n\\s*");
if( globs.length < 1 ){
ts.toss(argv[0], " requires 1 or more ",
(jsonMode ? "json snippets" : "globs"),".");
}
final String sql = t.takeInputBuffer();
t.execSql(null, true,
jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
ResultRowMode.NEWLINE, sql);
final String rbuf = t.getResultText();
final String[] res = rbuf.split("\n");
if( res.length != globs.length ){
ts.toss(argv[0], " failure: input has ", res.length,
" row(s) but expecting ",globs.length);
}
for(int i = 0; i < res.length; ++i){
final String glob = globs[i].replaceAll("\\s+"," ").trim();
//t.verbose(argv[0]," <<",glob,">> vs <<",res[i],">>");
if( jsonMode ){
if( !glob.equals(res[i]) ){
ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
res[i],">>");
}
}else if( 0 != SQLTester.strglob(glob, res[i]) ){
ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
}
}
}
}
//! --testcase command
class TestCaseCommand2 extends Command2 {
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
argcCheck(argv,1);
// TODO?: do something with the test name
final String body = ts.fetchCommandBody();
t.clearResultBuffer();
t.clearInputBuffer().append(null==body ? "" : body);
}
}
class CommandDispatcher2 {
private static java.util.Map<String,Command2> commandMap =
new java.util.HashMap<>();
/**
Returns a (cached) instance mapped to name, or null if no match
is found.
*/
static Command2 getCommandByName(String name){
Command2 rv = commandMap.get(name);
if( null!=rv ) return rv;
switch(name){
case "close": rv = new CloseDbCommand2(); break;
case "db": rv = new DbCommand2(); break;
case "glob": rv = new GlobCommand2(); break;
case "json": rv = new JsonCommand2(); break;
case "json-block": rv = new JsonBlockCommand2(); break;
case "new": rv = new NewDbCommand2(); break;
case "notglob": rv = new NotGlobCommand2(); break;
case "null": rv = new NullCommand2(); break;
case "oom": rv = new NoopCommand2(); break;
case "open": rv = new OpenDbCommand2(); break;
case "print": rv = new PrintCommand2(); break;
case "result": rv = new ResultCommand2(); break;
case "run": rv = new RunCommand2(); break;
case "tableresult": rv = new TableResultCommand2(); break;
case "testcase": rv = new TestCaseCommand2(); break;
default: rv = null; break;
}
if( null!=rv ) commandMap.put(name, rv);
return rv;
}
/**
Treats argv[0] as a command name, looks it up with
getCommandByName(), and calls process() on that instance, passing
it arguments given to this function.
*/
static void dispatch(SQLTester tester, TestScript2 ts, String[] argv) throws Exception{
final Command2 cmd = getCommandByName(argv[0]);
if(null == cmd){
if( tester.skipUnknownCommands() ){
ts.warn("skipping remainder because of unknown command '",argv[0],"'.");
throw new SkipTestRemainder2(ts);
}
Util.toss(IllegalArgumentException.class,
ts.getOutputPrefix()+": no command handler found for '"+argv[0]+"'.");
}
cmd.process(tester, ts, argv);
}
}
/**
This class represents a single test script. It handles (or
delegates) its the reading-in and parsing, but the details of
evaluation are delegated elsewhere.
*/
class TestScript2 {
private String filename = null;
private String moduleName = null;
private final Cursor cur = new Cursor();
private final Outer outer = new Outer();
private static final class Cursor {
private final StringBuilder sb = new StringBuilder();
byte[] src = null;
int pos = 0;
int putbackPos = 0;
int putbackLineNo = 0;
int lineNo = 0 /* yes, zero */;
int peekedPos = 0;
int peekedLineNo = 0;
boolean inComment = false;
void reset(){
sb.setLength(0); pos = 0; lineNo = 0/*yes, zero*/; inComment = false;
}
}
private byte[] readFile(String filename) throws Exception {
return java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filename));
}
/**
Initializes the script with the content of the given file.
Throws if it cannot read the file.
*/
public TestScript2(String filename) throws Exception{
this.filename = filename;
setVerbosity(2);
cur.src = readFile(filename);
}
public String getFilename(){
return filename;
}
public String getModuleName(){
return moduleName;
}
public void setVerbosity(int level){
outer.setVerbosity(level);
}
public String getOutputPrefix(){
return "["+(moduleName==null ? filename : moduleName)+"] line "+
cur.lineNo;
}
@SuppressWarnings("unchecked")
private TestScript2 verboseN(int level, Object... vals){
final int verbosity = outer.getVerbosity();
if(verbosity>=level){
outer.out("VERBOSE",(verbosity>1 ? "+ " : " "),
getOutputPrefix(),": ");
outer.outln(vals);
}
return this;
}
private TestScript2 verbose1(Object... vals){return verboseN(1,vals);}
private TestScript2 verbose2(Object... vals){return verboseN(2,vals);}
@SuppressWarnings("unchecked")
public TestScript2 warn(Object... vals){
outer.out("WARNING ", getOutputPrefix(),": ");
outer.outln(vals);
return this;
}
@SuppressWarnings("unchecked")
private void tossSyntax(Object... msg){
StringBuilder sb = new StringBuilder();
sb.append(this.filename).append(":").append(cur.lineNo).
append(": ");
for(Object o : msg) sb.append(o);
throw new RuntimeException(sb.toString());
}
private void reset(){
cur.reset();
}
/**
Returns the next line from the buffer, minus the trailing EOL.
Returns null when all input is consumed. Throws if it reads
illegally-encoded input, e.g. (non-)characters in the range
128-256.
*/
String getLine(){
if( cur.pos==cur.src.length ){
return null /* EOF */;
}
cur.putbackPos = cur.pos;
cur.putbackLineNo = cur.lineNo;
cur.sb.setLength(0);
final boolean skipLeadingWs = false;
byte b = 0, prevB = 0;
int i = cur.pos;
if(skipLeadingWs) {
/* Skip any leading spaces, including newlines. This will eliminate
blank lines. */
for(; i < cur.src.length; ++i, prevB=b){
b = cur.src[i];
switch((int)b){
case 32/*space*/: case 9/*tab*/: case 13/*CR*/: continue;
case 10/*NL*/: ++cur.lineNo; continue;
default: break;
}
break;
}
if( i==cur.src.length ){
return null /* EOF */;
}
}
boolean doBreak = false;
final byte[] aChar = {0,0,0,0} /* multi-byte char buffer */;
int nChar = 0 /* number of bytes in the char */;
for(; i < cur.src.length && !doBreak; ++i){
b = cur.src[i];
switch( (int)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. Appending individual bytes to the StringBuffer
appends their integer value. */
nChar = 1;
switch( b & 0xF0 ){
case 0xC0: nChar = 2; break;
case 0xE0: nChar = 3; break;
case 0xF0: nChar = 4; break;
default:
if( b > 127 ) tossSyntax("Invalid character (#"+(int)b+").");
break;
}
if( 1==nChar ){
cur.sb.append((char)b);
}else{
for(int x = 0; x < nChar; ++x) aChar[x] = cur.src[i+x];
cur.sb.append(new String(Arrays.copyOf(aChar, nChar),
StandardCharsets.UTF_8));
i += nChar-1;
}
break;
}
}
cur.pos = i;
final String rv = cur.sb.toString();
if( i==cur.src.length && 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.
*/
public String peekLine(){
final int oldPos = cur.pos;
final int oldPB = cur.putbackPos;
final int oldPBL = cur.putbackLineNo;
final int oldLine = cur.lineNo;
final String rc = 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().
*/
public void consumePeeked(){
cur.pos = cur.peekedPos;
cur.lineNo = cur.peekedLineNo;
}
/**
Restores the cursor to the position it had before the previous
call to getLine().
*/
public void putbackLine(){
cur.pos = cur.putbackPos;
cur.lineNo = cur.putbackLineNo;
}
private static final Pattern patternRequiredProperties =
Pattern.compile(" REQUIRED_PROPERTIES:[ \\t]*(\\S.*)\\s*$");
private static final Pattern patternScriptModuleName =
Pattern.compile(" SCRIPT_MODULE_NAME:[ \\t]*(\\S+)\\s*$");
private static final Pattern patternMixedModuleName =
Pattern.compile(" ((MIXED_)?MODULE_NAME):[ \\t]*(\\S+)\\s*$");
private static final Pattern patternCommand =
Pattern.compile("^--(([a-z-]+)( .*)?)$");
/**
Looks for "directives." If a compatible one is found, it is
processed and this function returns. If an incompatible one is found,
a description of it is returned and processing of the test must
end immediately.
*/
private void checkForDirective(String line) throws IncompatibleDirective {
if(line.startsWith("#")){
throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
}else if(line.startsWith("---")){
new IncompatibleDirective(this, "triple-dash: "+line);
}
Matcher m = patternScriptModuleName.matcher(line);
if( m.find() ){
moduleName = m.group(1);
return;
}
m = patternRequiredProperties.matcher(line);
if( m.find() ){
throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+m.group(1));
}
m = patternMixedModuleName.matcher(line);
if( m.find() ){
throw new IncompatibleDirective(this, m.group(1)+": "+m.group(3));
}
if( line.indexOf("\n|")>=0 ){
throw new IncompatibleDirective(this, "newline-pipe combination.");
}
return;
}
public boolean isCommandLine(String line, boolean checkForImpl){
final Matcher m = patternCommand.matcher(line);
boolean rc = m.find();
if( rc && checkForImpl ){
rc = null!=CommandDispatcher2.getCommandByName(m.group(2));
}
return rc;
}
/**
If line looks like a command, returns an argv for that command
invocation, else returns null.
*/
public String[] getCommandArgv(String line){
final Matcher m = patternCommand.matcher(line);
return m.find() ? m.group(1).trim().split("\\s+") : null;
}
/**
Fetches lines until the next recognized command. Throws if
checkForDirective() does. Returns null if there is no input or
it's only whitespace. The returned string retains all whitespace.
Note that "subcommands", --command-like constructs in the body
which do not match a known command name are considered to be
content, not commands.
*/
public String fetchCommandBody(){
final StringBuilder sb = new StringBuilder();
String line;
while( (null != (line = peekLine())) ){
checkForDirective(line);
if( !isCommandLine(line, true) ){
sb.append(line).append("\n");
consumePeeked();
}else{
break;
}
}
line = sb.toString();
return line.trim().isEmpty() ? null : line;
}
public void processCommand(SQLTester t, String[] argv) throws Exception{
verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
if(outer.getVerbosity()>1){
final String input = t.getInputText();
if( !input.isEmpty() ) verbose2("Input buffer = ",input);
}
CommandDispatcher2.dispatch(t, this, argv);
}
public void toss(Object... msg) throws TestScript2Failed {
StringBuilder sb = new StringBuilder();
for(Object s : msg) sb.append(s);
throw new TestScript2Failed(this, sb.toString());
}
/**
Runs this test script in the context of the given tester object.
*/
@SuppressWarnings("unchecked")
public boolean run(SQLTester tester) throws Exception {
reset();
setVerbosity(tester.getVerbosity());
String line, directive;
String[] argv;
while( null != (line = getLine()) ){
//verbose(line);
checkForDirective(line);
argv = getCommandArgv(line);
if( null!=argv ){
processCommand(tester, argv);
continue;
}
tester.appendInput(line,true);
}
return true;
}
}

View File

@ -8,6 +8,7 @@
** REQUIRED_PROPERTIES:
**
*/
--print starting up 😃
--close all
--oom
--db 0
@ -38,5 +39,13 @@ SELECT json_array(1,2,3)
[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
--print 🤩😃 the end
--print reached the end 😃

View File

@ -1,68 +0,0 @@
/* A script for testing the org.sqlite.jni.tester infrastructure
**
** SCRIPT_MODULE_NAME: 000_first
**
*/
junk
--new SQLTester.db
--run
SELECT 1;
SELECT 2;
-- comment
-- uncomment to introduce intentional syntax error
--oom
--print These are args to the print command.
This is from the print command's body.
--print
Also from the print command.
--- also ignored
--testcase 1
SELECT 'a b', 'c';
SELECT 'd', 'e';
SELECT '{}', 'f';
SELECT '{ }', 'g';
SELECT '(a-b-c)', '[a-b-c]';
-- this comment must not cause an error
--result {a b} c d e "{}" f "{\011}" g (a-b-c) [a-b-c]
--testcase 2
SELECT 123
--glob 1#
--testcase 3
SELECT 'a'
--notglob #
--close
--open SQLTester.db
--print Re-opened db.
--testcase fourth
SELECT 1, 2;
SELECT 'b', 'c';
--tableresult
[0-9] #
b c
--end
--null zilch
--testcase null-command
SELECT null;
--result zilch
--testcase json-array-1
SELECT json_array(1,2,3)
--json [1,2,3]
--testcase json-array-2
SELECT json_array(1,2,3);
SELECT json_object('a',1,'b',2);
--json-block
[1,2,3]
{"a":1,"b":2}
--end
--testcase table-result-globs
SELECT 123;
SELECT 'aBc';
SELECT 456;
--tableresult
#
[a-z][A-Z][a-z]
4#
--end
--an-unknown-command