]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Move all of the SQLTester code into a single file, since it's only got 1 public class...
authorstephan <stephan@noemail.net>
Thu, 10 Aug 2023 01:44:48 +0000 (01:44 +0000)
committerstephan <stephan@noemail.net>
Thu, 10 Aug 2023 01:44:48 +0000 (01:44 +0000)
FossilOrigin-Name: 2815d676951abdab674c374fd903486ea5796f8ee4cb338d41f19693419f8471

ext/jni/GNUmakefile
ext/jni/src/org/sqlite/jni/tester/Outer.java [deleted file]
ext/jni/src/org/sqlite/jni/tester/SQLTester.java
ext/jni/src/org/sqlite/jni/tester/TestScript.java [deleted file]
manifest
manifest.uuid

index 9d78f9b0e6843f055c72114172b88c35ce07ba4a..8506014462d19c382a78a60b7417a9f5fa0016ca 100644 (file)
@@ -83,11 +83,7 @@ ifeq (1,$(enable.fts5))
     TesterFts5.java \
   )
 endif
-JAVA_FILES.tester := $(patsubst %,$(dir.src.jni.tester)/%,\
-  Outer.java \
-  SQLTester.java \
-  TestScript.java \
-)
+JAVA_FILES.tester := $(dir.src.jni.tester)/SQLTester.java
 
 CLASS_FILES.main := $(JAVA_FILES.main:.java=.class)
 CLASS_FILES.tester := $(JAVA_FILES.tester:.java=.class)
diff --git a/ext/jni/src/org/sqlite/jni/tester/Outer.java b/ext/jni/src/org/sqlite/jni/tester/Outer.java
deleted file mode 100644 (file)
index e6f90dd..0000000
+++ /dev/null
@@ -1,60 +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 a utility class for generating console output.
-*/
-package org.sqlite.jni.tester;
-
-/**
-   Console output utility class.
-*/
-class Outer {
-  public int verbosity = 0;
-
-  public static void out(Object val){
-    System.out.print(val);
-  }
-
-  public static void outln(Object val){
-    System.out.println(val);
-  }
-
-  @SuppressWarnings("unchecked")
-  public static void out(Object... vals){
-    for(Object v : vals) out(v);
-  }
-
-  @SuppressWarnings("unchecked")
-  public static void outln(Object... vals){
-    out(vals);
-    out("\n");
-  }
-
-  @SuppressWarnings("unchecked")
-  public Outer verbose(Object... vals){
-    if(verbosity>0){
-      out("VERBOSE",(verbosity>1 ? "+: " : ": "));
-      outln(vals);
-    }
-    return this;
-  }
-
-  public void setVerbosity(int level){
-    verbosity = level;
-  }
-
-  public int getVerbosity(){
-    return verbosity;
-  }
-
-  public boolean isVerbose(){return verbosity > 0;}
-
-}
index f2c5da313234b6d4fa6a0aab0d77995abe6595c9..1e3443b718f3442af29de9e52f5a8291c0afc352 100644 (file)
@@ -20,12 +20,13 @@ import java.nio.charset.StandardCharsets;
 import java.util.regex.*;
 import org.sqlite.jni.*;
 import static org.sqlite.jni.SQLite3Jni.*;
+import org.sqlite.jni.sqlite3;
 
 
 /**
-   Modes for how to handle SQLTester.execSql()'s
-   result output.
- */
+   Modes for how to escape (or not) column values and names from
+   SQLTester.execSql() to the result buffer output.
+*/
 enum ResultBufferMode {
   //! Do not append to result buffer
   NONE,
@@ -35,6 +36,10 @@ enum ResultBufferMode {
   ASIS
 };
 
+/**
+   Modes to specify how to emit multi-row output from
+   SQLTester.execSql() to the result buffer.
+*/
 enum ResultRowMode {
   //! Keep all result rows on one line, space-separated.
   ONELINE,
@@ -48,6 +53,71 @@ class SQLTesterException extends RuntimeException {
   }
 }
 
+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);
+  }
+}
+
+/**
+   Console output utility class.
+*/
+class Outer {
+  private int verbosity = 0;
+
+  static void out(Object val){
+    System.out.print(val);
+  }
+
+  static void outln(Object val){
+    System.out.println(val);
+  }
+
+  @SuppressWarnings("unchecked")
+  Outer out(Object... vals){
+    for(Object v : vals) out(v);
+    return this;
+  }
+
+  @SuppressWarnings("unchecked")
+  Outer outln(Object... vals){
+    out(vals).out("\n");
+    return this;
+  }
+
+  @SuppressWarnings("unchecked")
+  Outer verbose(Object... vals){
+    if(verbosity>0){
+      out("VERBOSE",(verbosity>1 ? "+: " : ": "));
+      outln(vals);
+    }
+    return this;
+  }
+
+  void setVerbosity(int level){
+    verbosity = level;
+  }
+
+  int getVerbosity(){
+    return verbosity;
+  }
+
+  public boolean isVerbose(){return verbosity > 0;}
+
+}
+
 /**
    This class provides an application which aims to implement the
    rudimentary SQL-driven test tool described in the accompanying
@@ -64,45 +134,54 @@ public class SQLTester {
   private final StringBuilder inputBuffer = new StringBuilder();
   //! Test result buffer.
   private final StringBuilder resultBuffer = new StringBuilder();
-  private String nullView;
+  //! Output representation of SQL NULL.
+  private String nullView = "nil";
+  //! Total tests run.
   private int nTotalTest = 0;
+  //! Total test script files run.
   private int nTestFile = 0;
+  //! Number of scripts which were aborted.
   private int nAbortedScript = 0;
-  private int nTest;
+  //! Per-script test counter.
+  private int nTest = 0;
+  //! True to enable column name output from execSql()
   private boolean emitColNames;
+  //! The list of available db handles.
   private final sqlite3[] aDb = new sqlite3[7];
+  //! Index into aDb of the current db.
   private int iCurrentDb = 0;
+  //! Name of the default db, re-created for each script.
   private final String initialDbName = "test.db";
-  private TestScript currentScript;
+
 
   public SQLTester(){
     reset();
   }
 
-  public void setVerbosity(int level){
+  void setVerbosity(int level){
     this.outer.setVerbosity( level );
   }
-  public int getVerbosity(){
+  int getVerbosity(){
     return this.outer.getVerbosity();
   }
-  public boolean isVerbose(){
+  boolean isVerbose(){
     return this.outer.isVerbose();
   }
 
   void outputColumnNames(boolean b){ emitColNames = b; }
 
   @SuppressWarnings("unchecked")
-  public void verbose(Object... vals){
+  void verbose(Object... vals){
     outer.verbose(vals);
   }
 
   @SuppressWarnings("unchecked")
-  public void outln(Object... vals){
+  void outln(Object... vals){
     outer.outln(vals);
   }
 
   @SuppressWarnings("unchecked")
-  public void out(Object... vals){
+  void out(Object... vals){
     outer.out(vals);
   }
 
@@ -112,16 +191,12 @@ public class SQLTester {
     //verbose("Added file ",filename);
   }
 
-  public void setupInitialDb() throws Exception {
+  private void setupInitialDb() throws Exception {
     Util.unlink(initialDbName);
     openDb(0, initialDbName, true);
   }
 
-  TestScript getCurrentScript(){
-    return currentScript;
-  }
-
-  private void runTests() throws Exception {
+  public void runTests() throws Exception {
     for(String f : listInFiles){
       reset();
       setupInitialDb();
@@ -270,10 +345,12 @@ public class SQLTester {
 
   void incrementTestCounter(){ ++nTest; ++nTotalTest; }
 
+  //! "Special" characters - we have to escape output if it contains any.
   static final Pattern patternSpecial = Pattern.compile(
-    "[\\x00-\\x20\\x22\\x5c\\x7b\\x7d]", Pattern.MULTILINE
+    "[\\x00-\\x20\\x22\\x5c\\x7b\\x7d]"
   );
-  static final Pattern patternSquiggly = Pattern.compile("[{}]", Pattern.MULTILINE);
+  //! Either of '{' or '}'.
+  static final Pattern patternSquiggly = Pattern.compile("[{}]");
 
   /**
      Returns v or some escaped form of v, as defined in the tester's
@@ -317,6 +394,18 @@ public class SQLTester {
     }
   }
 
+  /**
+     Runs SQL on behalf of test commands and outputs the results following
+     the very specific rules of the test framework.
+
+     If db is null, getCurrentDb() is assumed. If throwOnError is true then
+     any db-side error will result in an exception, else they result in
+     the db's result code.
+
+     appendMode specifies how/whether to append results to the result
+     buffer. lineMode specifies whether to output all results in a
+     single line or one line per row.
+  */
   public int execSql(sqlite3 db, boolean throwOnError,
                      ResultBufferMode appendMode,
                      ResultRowMode lineMode,
@@ -462,14 +551,19 @@ public class SQLTester {
      of digits, e.g. "#23" or "1#3", but will match at the end,
      e.g. "12#".
   */
-  public static int strglob(String glob, String txt){
+  static int strglob(String glob, String txt){
     return strglob(
       (glob+"\0").getBytes(StandardCharsets.UTF_8),
       (txt+"\0").getBytes(StandardCharsets.UTF_8)
     );
   }
 
-  private static native void installCustomExtensions();
+  /**
+     Sets up C-side components needed by the test framework. This must
+     not be called until main() is triggered so that it does not
+     interfere with library clients who don't use this class.
+  */
+  static native void installCustomExtensions();
   static {
     System.loadLibrary("sqlite3-jni")
       /* Interestingly, when SQLTester is the main app, we have to
@@ -486,7 +580,7 @@ public class SQLTester {
 final class Util {
 
   //! Throws a new T, appending all msg args into a string for the message.
-  public static void toss(Class<? extends Exception> errorType, Object... msg) throws Exception {
+  static void toss(Class<? extends Exception> errorType, Object... msg) throws Exception {
     StringBuilder sb = new StringBuilder();
     for(Object s : msg) sb.append(s);
     final java.lang.reflect.Constructor<? extends Exception> ctor =
@@ -494,16 +588,12 @@ final class Util {
     throw ctor.newInstance(sb.toString());
   }
 
-  public static void toss(Object... msg) throws Exception{
+  static void toss(Object... msg) throws Exception{
     toss(RuntimeException.class, msg);
   }
 
-  public static void badArg(Object... msg) throws Exception{
-    toss(IllegalArgumentException.class, msg);
-  }
-
   //! Tries to delete the given file, silently ignoring failure.
-  public static void unlink(String filename){
+  static void unlink(String filename){
     try{
       final java.io.File f = new java.io.File(filename);
       f.delete();
@@ -514,10 +604,10 @@ final class Util {
 
   /**
      Appends all entries in argv[1..end] into a space-separated
-     string, argv[0] is not included because it's expected to
-     be a command name.
+     string, argv[0] is not included because it's expected to be a
+     command name.
   */
-  public static String argvToString(String[] argv){
+  static String argvToString(String[] argv){
     StringBuilder sb = new StringBuilder();
     for(int i = 1; i < argv.length; ++i ){
       if( i>1 ) sb.append(" ");
@@ -527,3 +617,648 @@ final class Util {
   }
 
 }
+
+/**
+   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,-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 = Util.argvToString(argv);
+    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(),": ");
+    if( 1==argv.length ){
+      st.out( st.getInputText() );
+    }else{
+      st.outln( Util.argvToString(argv) );
+    }
+  }
+}
+
+//! --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 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 TestScript(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 TestScript verboseN(int level, Object... vals){
+    final int verbosity = outer.getVerbosity();
+    if(verbosity>=level){
+      outer.out("VERBOSE",(verbosity>1 ? "+ " : " "),
+                getOutputPrefix(),": ")
+        .outln(vals);
+    }
+    return this;
+  }
+
+  private TestScript verbose1(Object... vals){return verboseN(1,vals);}
+  private TestScript verbose2(Object... vals){return verboseN(2,vals);}
+
+  @SuppressWarnings("unchecked")
+  public TestScript warn(Object... vals){
+    outer.out("WARNING ", getOutputPrefix(),": ")
+      .outln(vals);
+    return this;
+  }
+
+  private void reset(){
+    cur.reset();
+  }
+
+
+  /**
+     Returns the next line from the buffer, minus the trailing EOL.
+
+     Returns null when all input is consumed. Throws if it reads
+     illegally-encoded input, e.g. (non-)characters in the range
+     128-256.
+  */
+  String getLine(){
+    if( cur.pos==cur.src.length ){
+      return null /* EOF */;
+    }
+    cur.putbackPos = cur.pos;
+    cur.putbackLineNo = cur.lineNo;
+    cur.sb.setLength(0);
+    final boolean skipLeadingWs = false;
+    byte b = 0, prevB = 0;
+    int i = cur.pos;
+    if(skipLeadingWs) {
+      /* Skip any leading spaces, including newlines. This will eliminate
+         blank lines. */
+      for(; i < cur.src.length; ++i, prevB=b){
+        b = cur.src[i];
+        switch((int)b){
+          case 32/*space*/: case 9/*tab*/: case 13/*CR*/: continue;
+          case 10/*NL*/: ++cur.lineNo; continue;
+          default: break;
+        }
+        break;
+      }
+      if( i==cur.src.length ){
+        return null /* EOF */;
+      }
+    }
+    boolean doBreak = false;
+    final byte[] aChar = {0,0,0,0} /* multi-byte char buffer */;
+    int nChar = 0 /* number of bytes in the char */;
+    for(; i < cur.src.length && !doBreak; ++i){
+      b = cur.src[i];
+      switch( (int)b ){
+        case 13/*CR*/: continue;
+        case 10/*NL*/:
+          ++cur.lineNo;
+          if(cur.sb.length()>0) doBreak = true;
+          // Else it's an empty string
+          break;
+        default:
+          /* Multi-byte chars need to be gathered up and appended at
+             one time. Appending individual bytes to the StringBuffer
+             appends their integer value. */
+          nChar = 1;
+          switch( b & 0xF0 ){
+            case 0xC0: nChar = 2; break;
+            case 0xE0: nChar = 3; break;
+            case 0xF0: nChar = 4; break;
+            default:
+              if( b > 127 ) this.toss("Invalid character (#"+(int)b+").");
+              break;
+          }
+          if( 1==nChar ){
+            cur.sb.append((char)b);
+          }else{
+            for(int x = 0; x < nChar; ++x) aChar[x] = cur.src[i+x];
+            cur.sb.append(new String(Arrays.copyOf(aChar, nChar),
+                                      StandardCharsets.UTF_8));
+            i += nChar-1;
+          }
+          break;
+      }
+    }
+    cur.pos = i;
+    final String rv = cur.sb.toString();
+    if( i==cur.src.length && 0==rv.length() ){
+      return null /* EOF */;
+    }
+    return rv;
+  }/*getLine()*/
+
+  /**
+     Fetches the next line then resets the cursor to its pre-call
+     state. consumePeeked() can be used to consume this peeked line
+     without having to re-parse it.
+  */
+  String peekLine(){
+    final int oldPos = cur.pos;
+    final int oldPB = cur.putbackPos;
+    final int oldPBL = cur.putbackLineNo;
+    final int oldLine = cur.lineNo;
+    final String rc = getLine();
+    cur.peekedPos = cur.pos;
+    cur.peekedLineNo = cur.lineNo;
+    cur.pos = oldPos;
+    cur.lineNo = oldLine;
+    cur.putbackPos = oldPB;
+    cur.putbackLineNo = oldPBL;
+    return rc;
+  }
+
+  /**
+     Only valid after calling peekLine() and before calling getLine().
+     This places the cursor to the position it would have been at had
+     the peekLine() had been fetched with getLine().
+  */
+  void consumePeeked(){
+    cur.pos = cur.peekedPos;
+    cur.lineNo = cur.peekedLineNo;
+  }
+
+  /**
+     Restores the cursor to the position it had before the previous
+     call to getLine().
+  */
+  void putbackLine(){
+    cur.pos = cur.putbackPos;
+    cur.lineNo = cur.putbackLineNo;
+  }
+
+  private static final Pattern patternRequiredProperties =
+    Pattern.compile(" REQUIRED_PROPERTIES:[ \\t]*(\\S.*)\\s*$");
+  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;
+  }
+
+  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.
+  */
+  String[] getCommandArgv(String line){
+    final Matcher m = patternCommand.matcher(line);
+    return m.find() ? m.group(1).trim().split("\\s+") : null;
+  }
+
+  /**
+     Fetches lines until the next recognized command. Throws if
+     checkForDirective() does.  Returns null if there is no input or
+     it's only whitespace. The returned string retains all whitespace.
+
+     Note that "subcommands", --command-like constructs in the body
+     which do not match a known command name are considered to be
+     content, not commands.
+  */
+  String fetchCommandBody(){
+    final StringBuilder sb = new StringBuilder();
+    String line;
+    while( (null != (line = peekLine())) ){
+      checkForDirective(line);
+      if( !isCommandLine(line, true) ){
+        sb.append(line).append("\n");
+        consumePeeked();
+      }else{
+        break;
+      }
+    }
+    line = sb.toString();
+    return line.trim().isEmpty() ? null : line;
+  }
+
+  private void processCommand(SQLTester t, String[] argv) throws Exception{
+    verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
+    if(outer.getVerbosity()>1){
+      final String input = t.getInputText();
+      if( !input.isEmpty() ) verbose2("Input buffer = ",input);
+    }
+    CommandDispatcher2.dispatch(t, this, argv);
+  }
+
+  void toss(Object... msg) throws TestScriptFailed {
+    StringBuilder sb = new StringBuilder();
+    for(Object s : msg) sb.append(s);
+    throw new TestScriptFailed(this, sb.toString());
+  }
+
+  /**
+     Runs this test script in the context of the given tester object.
+  */
+  @SuppressWarnings("unchecked")
+  public boolean run(SQLTester tester) throws Exception {
+    reset();
+    setVerbosity(tester.getVerbosity());
+    String line, directive;
+    String[] argv;
+    while( null != (line = getLine()) ){
+      //verbose(line);
+      checkForDirective(line);
+      argv = getCommandArgv(line);
+      if( null!=argv ){
+        processCommand(tester, argv);
+        continue;
+      }
+      tester.appendInput(line,true);
+    }
+    return true;
+  }
+}
diff --git a/ext/jni/src/org/sqlite/jni/tester/TestScript.java b/ext/jni/src/org/sqlite/jni/tester/TestScript.java
deleted file mode 100644 (file)
index c075ceb..0000000
+++ /dev/null
@@ -1,682 +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 TestScript 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 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,-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 = Util.argvToString(argv);
-    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(),": ");
-    if( 1==argv.length ){
-      st.out( st.getInputText() );
-    }else{
-      st.outln( Util.argvToString(argv) );
-    }
-  }
-}
-
-//! --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 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 TestScript(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 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);}
-
-  @SuppressWarnings("unchecked")
-  public TestScript warn(Object... vals){
-    outer.out("WARNING ", getOutputPrefix(),": ");
-    outer.outln(vals);
-    return this;
-  }
-
-  private void reset(){
-    cur.reset();
-  }
-
-
-  /**
-     Returns the next line from the buffer, minus the trailing EOL.
-
-     Returns null when all input is consumed. Throws if it reads
-     illegally-encoded input, e.g. (non-)characters in the range
-     128-256.
-  */
-  String getLine(){
-    if( cur.pos==cur.src.length ){
-      return null /* EOF */;
-    }
-    cur.putbackPos = cur.pos;
-    cur.putbackLineNo = cur.lineNo;
-    cur.sb.setLength(0);
-    final boolean skipLeadingWs = false;
-    byte b = 0, prevB = 0;
-    int i = cur.pos;
-    if(skipLeadingWs) {
-      /* Skip any leading spaces, including newlines. This will eliminate
-         blank lines. */
-      for(; i < cur.src.length; ++i, prevB=b){
-        b = cur.src[i];
-        switch((int)b){
-          case 32/*space*/: case 9/*tab*/: case 13/*CR*/: continue;
-          case 10/*NL*/: ++cur.lineNo; continue;
-          default: break;
-        }
-        break;
-      }
-      if( i==cur.src.length ){
-        return null /* EOF */;
-      }
-    }
-    boolean doBreak = false;
-    final byte[] aChar = {0,0,0,0} /* multi-byte char buffer */;
-    int nChar = 0 /* number of bytes in the char */;
-    for(; i < cur.src.length && !doBreak; ++i){
-      b = cur.src[i];
-      switch( (int)b ){
-        case 13/*CR*/: continue;
-        case 10/*NL*/:
-          ++cur.lineNo;
-          if(cur.sb.length()>0) doBreak = true;
-          // Else it's an empty string
-          break;
-        default:
-          /* Multi-byte chars need to be gathered up and appended at
-             one time. Appending individual bytes to the StringBuffer
-             appends their integer value. */
-          nChar = 1;
-          switch( b & 0xF0 ){
-            case 0xC0: nChar = 2; break;
-            case 0xE0: nChar = 3; break;
-            case 0xF0: nChar = 4; break;
-            default:
-              if( b > 127 ) this.toss("Invalid character (#"+(int)b+").");
-              break;
-          }
-          if( 1==nChar ){
-            cur.sb.append((char)b);
-          }else{
-            for(int x = 0; x < nChar; ++x) aChar[x] = cur.src[i+x];
-            cur.sb.append(new String(Arrays.copyOf(aChar, nChar),
-                                      StandardCharsets.UTF_8));
-            i += nChar-1;
-          }
-          break;
-      }
-    }
-    cur.pos = i;
-    final String rv = cur.sb.toString();
-    if( i==cur.src.length && 0==rv.length() ){
-      return null /* EOF */;
-    }
-    return rv;
-  }/*getLine()*/
-
-  /**
-     Fetches the next line then resets the cursor to its pre-call
-     state. consumePeeked() can be used to consume this peeked line
-     without having to re-parse it.
-  */
-  String peekLine(){
-    final int oldPos = cur.pos;
-    final int oldPB = cur.putbackPos;
-    final int oldPBL = cur.putbackLineNo;
-    final int oldLine = cur.lineNo;
-    final String rc = getLine();
-    cur.peekedPos = cur.pos;
-    cur.peekedLineNo = cur.lineNo;
-    cur.pos = oldPos;
-    cur.lineNo = oldLine;
-    cur.putbackPos = oldPB;
-    cur.putbackLineNo = oldPBL;
-    return rc;
-  }
-
-  /**
-     Only valid after calling peekLine() and before calling getLine().
-     This places the cursor to the position it would have been at had
-     the peekLine() had been fetched with getLine().
-  */
-  void consumePeeked(){
-    cur.pos = cur.peekedPos;
-    cur.lineNo = cur.peekedLineNo;
-  }
-
-  /**
-     Restores the cursor to the position it had before the previous
-     call to getLine().
-  */
-  void putbackLine(){
-    cur.pos = cur.putbackPos;
-    cur.lineNo = cur.putbackLineNo;
-  }
-
-  private static final Pattern patternRequiredProperties =
-    Pattern.compile(" REQUIRED_PROPERTIES:[ \\t]*(\\S.*)\\s*$");
-  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;
-  }
-
-  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.
-  */
-  String[] getCommandArgv(String line){
-    final Matcher m = patternCommand.matcher(line);
-    return m.find() ? m.group(1).trim().split("\\s+") : null;
-  }
-
-  /**
-     Fetches lines until the next recognized command. Throws if
-     checkForDirective() does.  Returns null if there is no input or
-     it's only whitespace. The returned string retains all whitespace.
-
-     Note that "subcommands", --command-like constructs in the body
-     which do not match a known command name are considered to be
-     content, not commands.
-  */
-  String fetchCommandBody(){
-    final StringBuilder sb = new StringBuilder();
-    String line;
-    while( (null != (line = peekLine())) ){
-      checkForDirective(line);
-      if( !isCommandLine(line, true) ){
-        sb.append(line).append("\n");
-        consumePeeked();
-      }else{
-        break;
-      }
-    }
-    line = sb.toString();
-    return line.trim().isEmpty() ? null : line;
-  }
-
-  private void processCommand(SQLTester t, String[] argv) throws Exception{
-    verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
-    if(outer.getVerbosity()>1){
-      final String input = t.getInputText();
-      if( !input.isEmpty() ) verbose2("Input buffer = ",input);
-    }
-    CommandDispatcher2.dispatch(t, this, argv);
-  }
-
-  void toss(Object... msg) throws TestScriptFailed {
-    StringBuilder sb = new StringBuilder();
-    for(Object s : msg) sb.append(s);
-    throw new TestScriptFailed(this, sb.toString());
-  }
-
-  /**
-     Runs this test script in the context of the given tester object.
-  */
-  @SuppressWarnings("unchecked")
-  public boolean run(SQLTester tester) throws Exception {
-    reset();
-    setVerbosity(tester.getVerbosity());
-    String line, directive;
-    String[] argv;
-    while( null != (line = getLine()) ){
-      //verbose(line);
-      checkForDirective(line);
-      argv = getCommandArgv(line);
-      if( null!=argv ){
-        processCommand(tester, argv);
-        continue;
-      }
-      tester.appendInput(line,true);
-    }
-    return true;
-  }
-}
index 83a8306b8c5251fa72e2d8be13339aaa40bd67df..0714a98a0b424edff445cd7129fe6093518c695d 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Defer\sstatic\sJNI-side\sinit\sof\sSQLTester\suntil\smain()\sis\scalled\sso\sthat\sits\sauto-extensions\sdo\snot\sleak\sover\sto\sclients\sof\sthe\smain\slibrary.
-D 2023-08-10T01:19:40.795
+C Move\sall\sof\sthe\sSQLTester\scode\sinto\sa\ssingle\sfile,\ssince\sit's\sonly\sgot\s1\spublic\sclass.\sRemove\s'public'\sfrom\smany\smethods\swhich\sdon't\sneed\sit.\sAdd\smore\sdocumentation\sto\sit.
+D 2023-08-10T01:44:48.660
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -231,7 +231,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 52f402abb8c4695a58f734d20455cf1a5afaaa10ceacc47bcbf1b06a8d5d27e8
+F ext/jni/GNUmakefile dcaf23ca24ee2ec2d2e36afdc58886fcf147830c25989a669599c69d38796493
 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,9 +266,7 @@ F ext/jni/src/org/sqlite/jni/sqlite3.java 62b1b81935ccf3393472d17cb883dc5ff39c38
 F ext/jni/src/org/sqlite/jni/sqlite3_context.java d26573fc7b309228cb49786e9078597d96232257defa955a3425d10897bca810
 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 a5d843ade1f4456cf5cd3d021f5593ff60c0d8786fb55a2d59d7a984738c299e
-F ext/jni/src/org/sqlite/jni/tester/TestScript.java 9f172fcffae9935c6d1c9686b3f69df199a191b7280c926bf85fa8fa30ebbddf
+F ext/jni/src/org/sqlite/jni/tester/SQLTester.java e7148d734ee13baa8a49508a9a7890914d8d57ff8353fce78b60a875f407cf91
 F ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md ab7169b08566a082ef55c9ef8a553827f99958ed3e076f31eef757563fae51ba
 F ext/jni/src/tests/000-000-sanity.test de89692155bee1bb35120aced6871dd6562014d0cd7c1dcf173297d8bbc03002
 F ext/jni/src/tests/000-001-ignored.test e17e874c6ab3c437f1293d88093cf06286083b65bf162317f91bbfd92f961b70
@@ -2091,8 +2089,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 52fa6f78414c41073431c166550806bb8a835bd38cfc1236c9363784c78b81b9
-R e4a7dc0518dd6056979335c8c6e73e83
+P e461fdd53bd3212bee24ec5f5d5c234011ab30f3f67e115de9f85fdb760e3848
+R 4272692fbef77fb1007b0ab4a881bf94
 U stephan
-Z 125c5fe250744e87fc135cd6f93efe47
+Z 62b15385eb7331c7503b86f0cead2232
 # Remove this line to create a well-formed Fossil manifest.
index 904da66b33694a40325ba2ca375b7ced3a8f4c44..f0f33178116c24487bf179009c5a4dc49bab8a29 100644 (file)
@@ -1 +1 @@
-e461fdd53bd3212bee24ec5f5d5c234011ab30f3f67e115de9f85fdb760e3848
\ No newline at end of file
+2815d676951abdab674c374fd903486ea5796f8ee4cb338d41f19693419f8471
\ No newline at end of file