mirror of
https://github.com/sqlite/sqlite.git
synced 2025-07-29 08:01:23 +03:00
Move all of the SQLTester code into a single file, since it's only got 1 public class. Remove 'public' from many methods which don't need it. Add more documentation to it.
FossilOrigin-Name: 2815d676951abdab674c374fd903486ea5796f8ee4cb338d41f19693419f8471
This commit is contained in:
@ -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;}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user