1
0
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:
stephan
2023-08-10 01:44:48 +00:00
parent 2a91065145
commit 0c6df29cba
6 changed files with 776 additions and 789 deletions

View File

@ -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;}
}

View 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;
}
}

View File

@ -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;
}
}