Outer.java \
SQLTester.java \
TestScript.java \
- TestScript2.java \
)
CLASS_FILES.main := $(JAVA_FILES.main:.java=.class)
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
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
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";
return this.outer.isVerbose();
}
+ void outputColumnNames(boolean b){ emitColNames = b; }
+
@SuppressWarnings("unchecked")
public void verbose(Object... vals){
outer.verbose(vals);
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);
}
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;
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;
+ 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;
+ }
+ if( null!=sb ){
+ // 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.");
+ }
}
- 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( 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;
}
- }else{
- while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){}
}
+ }finally{
sqlite3_finalize(stmt);
- if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
- else if( rc!=0 ){
- if( null!=sb ){
- appendDbErr(db, sb, rc);
- }
- break;
- }
}
- sqlite3_finalize(stmt);
if( 0!=rc && throwOnError ){
Util.toss(RuntimeException.class, "db op failed with rc="
+rc+": "+sqlite3_errmsg(db));
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);
}
}
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).");
+ }
}
}
}
-/**
- 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.
*/
** 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 {
/**
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());
+ public String getFilename(){
+ return filename;
}
- private void setContent(String c){
- this.chunks = chunkContent(c);
+ public String getModuleName(){
+ return moduleName;
}
- public String getName(){
- return name;
+ public void setVerbosity(int level){
+ outer.setVerbosity(level);
}
- public String getModuleName(){
- return moduleName;
+ public String getOutputPrefix(){
+ return "["+(moduleName==null ? filename : moduleName)+"] line "+
+ cur.lineNo;
}
+ @SuppressWarnings("unchecked")
+ 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 TestScript verbose1(Object... vals){return verboseN(1,vals);}
+ private TestScript verbose2(Object... vals){return verboseN(2,vals);}
- public boolean isIgnored(){
- return null!=ignoreReason;
+ @SuppressWarnings("unchecked")
+ public TestScript warn(Object... vals){
+ outer.out("WARNING ", getOutputPrefix(),": ");
+ outer.outln(vals);
+ return this;
}
- public String getIgnoredReason(){
- return ignoreReason;
+ private void reset(){
+ cur.reset();
}
- public void setVerbosity(int level){
- outer.setVerbosity(level);
+
+ /**
+ 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;
}
- @SuppressWarnings("unchecked")
- private TestScript verbose(Object... vals){
- outer.verbose(vals);
- return this;
+ /**
+ 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 patternHashLine =
- Pattern.compile("^#", Pattern.MULTILINE);
private static final Pattern patternRequiredProperties =
- Pattern.compile("REQUIRED_PROPERTIES:[ \\t]*(\\S+\\s*)\\n");
+ 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-]+)( .*)?)$");
+
/**
- Returns true if the given script content should be ignored
- (because it contains certain content which indicates such).
+ 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 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;
+ 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 = patternHashLine.matcher(content);
+ Matcher m = patternScriptModuleName.matcher(line);
if( m.find() ){
- ignoreReason = "C-preprocessor line found.";
- return true;
+ moduleName = m.group(1);
+ return;
}
- m = patternRequiredProperties.matcher(content);
+ m = patternRequiredProperties.matcher(line);
if( m.find() ){
- ignoreReason = "REQUIRED_PROPERTIES found: "+m.group(1).trim();
- return true;
+ throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+m.group(1));
}
- return false;
+ 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;
}
- 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;
+ 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;
}
/**
- 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.
+ If line looks like a command, returns an argv for that command
+ invocation, else returns null.
+ */
+ String[] getCommandArgv(String line){
+ final Matcher m = patternCommand.matcher(line);
+ return m.find() ? m.group(1).trim().split("\\s+") : null;
+ }
- - It loses all file location information, so we can't report line
- numbers of errors.
+ /**
+ 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.
- If/when that becomes a problem, it can be refactored.
+ Note that "subcommands", --command-like constructs in the body
+ which do not match a known command name are considered to be
+ content, not commands.
*/
- private List<CommandChunk> chunkContent(String content){
- findModuleName(content);
- if( shouldBeIgnored(content) ){
- chunks = null;
- return null;
- }
-
- // 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 );
+ 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;
}
}
- // 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 );
+ 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);
}
- return rc;
+ 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.
*/
- 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()
- );
+ @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;
}
}
+++ /dev/null
-/*
-** 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;
- }
-}
** REQUIRED_PROPERTIES:
**
*/
+--print starting up 😃
--close all
--oom
--db 0
[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 😃
+++ /dev/null
-/* 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
-C Port\sthe\sSQLTester\s'v1'\scommands\sto\sthe\s'v2'\sevaluation\sbits.\sStill\sTODO\sis\sswapping\sout\sv1\swith\sthese\sseparate\simpls.
-D 2023-08-09T23:47:14.521
+C Replace\sthe\sSQLTester\sinfrastructure\swith\sa\sline-oriented,\snon-regex-heavy\sparser.\sAdd\s--column-names\scommand.
+D 2023-08-10T00:34:38.136
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
F ext/icu/README.txt 7ab7ced8ae78e3a645b57e78570ff589d4c672b71370f5aa9e1cd7024f400fc9
F ext/icu/icu.c c074519b46baa484bb5396c7e01e051034da8884bad1a1cb7f09bbe6be3f0282
F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a32075a8
-F ext/jni/GNUmakefile d7300b7e124214afde7f11bddd5c0d336a9be0220fe2b74e787078e1aa2db778
+F ext/jni/GNUmakefile 52f402abb8c4695a58f734d20455cf1a5afaaa10ceacc47bcbf1b06a8d5d27e8
F ext/jni/README.md e965674505e105626127ad45e628e4d19fcd379cdafc4d23c814c1ac2c55681d
F ext/jni/src/c/sqlite3-jni.c bae09ff8bf45f19a506a4eaaf693d26b81f0dd0a410b82475e04dde4b1c5a520
F ext/jni/src/c/sqlite3-jni.h 84a3fc3d308e347a2f6b24e4cb8bbafdfa8e75361302047d788e51a307cb2328
F ext/jni/src/org/sqlite/jni/sqlite3_stmt.java 78e6d1b95ac600a9475e9db4623f69449322b0c93d1bd4e1616e76ed547ed9fc
F ext/jni/src/org/sqlite/jni/sqlite3_value.java 3d1d4903e267bc0bc81d57d21f5e85978eff389a1a6ed46726dbe75f85e6914a
F ext/jni/src/org/sqlite/jni/tester/Outer.java b06acf9c79e8dbc8fea4a98b00724a6a76e3ee4503eb114671d2885f8fb3df8b
-F ext/jni/src/org/sqlite/jni/tester/SQLTester.java 1ae38d872d2cb582e1a1abd67b5e9c276bf2f610cacc918428b63c668131642e
-F ext/jni/src/org/sqlite/jni/tester/TestScript.java 463021981a65ffe7147a1bfada557b275b0cba3c33176ac328502ff09d146f28
-F ext/jni/src/org/sqlite/jni/tester/TestScript2.java 25895a534a1e4634268beecd1a689bdfc0aafbfe32071c27b5189ccb8aeec31e
+F ext/jni/src/org/sqlite/jni/tester/SQLTester.java e6e4a1f78291f9b76284035dacc3d77a85f8d1a8791d7acaf201deffd771d354
+F ext/jni/src/org/sqlite/jni/tester/TestScript.java 496b402c7faedf18be41542c6dc77c19f2735663821a5973639eb614e33aa707
F ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md ab7169b08566a082ef55c9ef8a553827f99958ed3e076f31eef757563fae51ba
-F ext/jni/src/tests/000-000-sanity.test2 dfbcccc7b3548ae56deb2ef8fe17dd9235a81fbd552536ef9202284549c7fcf3
-F ext/jni/src/tests/000_first.test cd5fb732520cf36d7a3e5ad94a274c7327a9504b01a1a7f98e1f946df6c539fd
-F ext/jni/src/tests/010_ignored.test e17e874c6ab3c437f1293d88093cf06286083b65bf162317f91bbfd92f961b70
+F ext/jni/src/tests/000-000-sanity.test de89692155bee1bb35120aced6871dd6562014d0cd7c1dcf173297d8bbc03002 w ext/jni/src/tests/000-000-sanity.test2
+F ext/jni/src/tests/000-001-ignored.test e17e874c6ab3c437f1293d88093cf06286083b65bf162317f91bbfd92f961b70 w ext/jni/src/tests/010_ignored.test
F ext/lsm1/Makefile a553b728bba6c11201b795188c5708915cc4290f02b7df6ba7e8c4c943fd5cd9
F ext/lsm1/Makefile.msc f8c878b467232226de288da320e1ac71c131f5ec91e08b21f502303347260013
F ext/lsm1/lsm-test/README 87ea529d2abe615e856d4714bfe8bb185e6c2771b8612aa6298588b7b43e6f86
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P 7a19bef4f572a90fb7896b9360f9c72b052955ca9b0549be870b2b245c1f1b2b
-R eed3d04c8636ba991a620a2a8f5a013d
+P 0cf57e5b0f90779e450e9db1ca009610df5e6f4487337d49017636bde3bb02d6
+R aaacd016e2b2dfc7887fd83bed604d98
U stephan
-Z bdeddd00da26b413ab473bf39ad731fc
+Z fb4c2d6a86f69138bd39103974c9d8cc
# Remove this line to create a well-formed Fossil manifest.
-0cf57e5b0f90779e450e9db1ca009610df5e6f4487337d49017636bde3bb02d6
\ No newline at end of file
+88863908ee2059c2d18a095cbd91f41674c7b0d0a8864ec21715a5317054df4d
\ No newline at end of file