]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Replace the SQLTester infrastructure with a line-oriented, non-regex-heavy parser...
authorstephan <stephan@noemail.net>
Thu, 10 Aug 2023 00:34:38 +0000 (00:34 +0000)
committerstephan <stephan@noemail.net>
Thu, 10 Aug 2023 00:34:38 +0000 (00:34 +0000)
FossilOrigin-Name: 88863908ee2059c2d18a095cbd91f41674c7b0d0a8864ec21715a5317054df4d

ext/jni/GNUmakefile
ext/jni/src/org/sqlite/jni/tester/SQLTester.java
ext/jni/src/org/sqlite/jni/tester/TestScript.java
ext/jni/src/org/sqlite/jni/tester/TestScript2.java [deleted file]
ext/jni/src/tests/000-000-sanity.test [moved from ext/jni/src/tests/000-000-sanity.test2 with 76% similarity]
ext/jni/src/tests/000-001-ignored.test [moved from ext/jni/src/tests/010_ignored.test with 100% similarity]
ext/jni/src/tests/000_first.test [deleted file]
manifest
manifest.uuid

index 6d7f35f9006bd9d933308a38c8278731a36977db..9d78f9b0e6843f055c72114172b88c35ce07ba4a 100644 (file)
@@ -87,7 +87,6 @@ JAVA_FILES.tester := $(patsubst %,$(dir.src.jni.tester)/%,\
   Outer.java \
   SQLTester.java \
   TestScript.java \
-  TestScript2.java \
 )
 
 CLASS_FILES.main := $(JAVA_FILES.main:.java=.class)
index 03bde86fdd96e5ed82b99abe83dbff4542d22d36..431679c7cee743f5926f9ac7bb041b7eadfbc2d7 100644 (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;
+        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));
@@ -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.
 */
index 742cf4fa52d1c595ced1ef2227c67be15364aafc..8f6863eef0d51851b5ae7fdf2f2ef1aa6c6d0773 100644 (file)
 ** 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());
+  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;
   }
 }
diff --git a/ext/jni/src/org/sqlite/jni/tester/TestScript2.java b/ext/jni/src/org/sqlite/jni/tester/TestScript2.java
deleted file mode 100644 (file)
index ee85b72..0000000
+++ /dev/null
@@ -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;
-  }
-}
similarity index 76%
rename from ext/jni/src/tests/000-000-sanity.test2
rename to ext/jni/src/tests/000-000-sanity.test
index 3ad4c396802bd8db474f4dd6a901fd3c63a6dc4b..905882dc3b8d3d7b2c9404210156678d5b67a4de 100644 (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 ðŸ˜ƒ
diff --git a/ext/jni/src/tests/000_first.test b/ext/jni/src/tests/000_first.test
deleted file mode 100644 (file)
index bcd78a2..0000000
+++ /dev/null
@@ -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
index 8119a6760572e16c3bb5e779130b31f26e080162..8b758a7b5fae649e4f7a56dc1e8c944c369e9de1 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-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
@@ -230,7 +230,7 @@ F ext/fts5/tool/showfts5.tcl d54da0e067306663e2d5d523965ca487698e722c
 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
@@ -266,13 +266,11 @@ F ext/jni/src/org/sqlite/jni/sqlite3_context.java d26573fc7b309228cb49786e907859
 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
@@ -2092,8 +2090,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 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.
index d522acb938745fce063d55019bf5177104646dc5..2507153deed0db6abcf95163539924d4777beee0 100644 (file)
@@ -1 +1 @@
-0cf57e5b0f90779e450e9db1ca009610df5e6f4487337d49017636bde3bb02d6
\ No newline at end of file
+88863908ee2059c2d18a095cbd91f41674c7b0d0a8864ec21715a5317054df4d
\ No newline at end of file