mirror of
https://github.com/sqlite/sqlite.git
synced 2025-07-30 19:03:16 +03:00
Replace the SQLTester infrastructure with a line-oriented, non-regex-heavy parser. Add --column-names command.
FossilOrigin-Name: 88863908ee2059c2d18a095cbd91f41674c7b0d0a8864ec21715a5317054df4d
This commit is contained in:
@ -21,19 +21,6 @@ import java.util.regex.*;
|
||||
import org.sqlite.jni.*;
|
||||
import static org.sqlite.jni.SQLite3Jni.*;
|
||||
|
||||
class TestFailure extends RuntimeException {
|
||||
public TestFailure(String msg){
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class SkipTestRemainder extends RuntimeException {
|
||||
public TestScript testScript;
|
||||
public SkipTestRemainder(TestScript ts){
|
||||
super("Skipping remainder of "+ts.getName());
|
||||
testScript = ts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Modes for how to handle SQLTester.execSql()'s
|
||||
@ -55,6 +42,12 @@ enum ResultRowMode {
|
||||
NEWLINE
|
||||
};
|
||||
|
||||
class SQLTesterException extends RuntimeException {
|
||||
public SQLTesterException(String msg){
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This class provides an application which aims to implement the
|
||||
rudimentary SQL-driven test tool described in the accompanying
|
||||
@ -76,6 +69,7 @@ public class SQLTester {
|
||||
private int nTestFile = 0;
|
||||
private int nAbortedScript = 0;
|
||||
private int nTest;
|
||||
private boolean emitColNames;
|
||||
private final sqlite3[] aDb = new sqlite3[7];
|
||||
private int iCurrentDb = 0;
|
||||
private final String initialDbName = "test.db";
|
||||
@ -95,6 +89,8 @@ public class SQLTester {
|
||||
return this.outer.isVerbose();
|
||||
}
|
||||
|
||||
void outputColumnNames(boolean b){ emitColNames = b; }
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void verbose(Object... vals){
|
||||
outer.verbose(vals);
|
||||
@ -125,56 +121,29 @@ public class SQLTester {
|
||||
return currentScript;
|
||||
}
|
||||
|
||||
public void runTests() throws Exception {
|
||||
// process each input file
|
||||
try {
|
||||
for(String f : listInFiles){
|
||||
reset();
|
||||
setupInitialDb();
|
||||
++nTestFile;
|
||||
final TestScript ts = new TestScript(f);
|
||||
currentScript = ts;
|
||||
outln("----->>>>> ",ts.getModuleName()," [",ts.getName(),"]");
|
||||
if( ts.isIgnored() ){
|
||||
outln("WARNING: skipping [",ts.getModuleName(),"]: ",
|
||||
ts.getIgnoredReason());
|
||||
continue;
|
||||
}else{
|
||||
try{
|
||||
ts.run(this);
|
||||
}catch(SkipTestRemainder e){
|
||||
/* not an error */
|
||||
++nAbortedScript;
|
||||
}
|
||||
}
|
||||
outln("<<<<<----- ",ts.getModuleName(),": ",nTest," test(s)");
|
||||
}
|
||||
}finally{
|
||||
currentScript = null;
|
||||
}
|
||||
Util.unlink(initialDbName);
|
||||
}
|
||||
|
||||
|
||||
//! Not yet funcional
|
||||
private void runTests2() throws Exception {
|
||||
private void runTests() throws Exception {
|
||||
for(String f : listInFiles){
|
||||
reset();
|
||||
setupInitialDb();
|
||||
++nTestFile;
|
||||
final TestScript2 ts = new TestScript2(f);
|
||||
final TestScript ts = new TestScript(f);
|
||||
outln("----->>>>> running [",f,"]");
|
||||
try{
|
||||
ts.run(this);
|
||||
}catch(SkipTestRemainder2 e){
|
||||
/* not fatal */
|
||||
}catch(UnknownCommand e){
|
||||
/* currently not fatal */
|
||||
outln(e);
|
||||
++nAbortedScript;
|
||||
}catch(IncompatibleDirective e){
|
||||
/* not fatal */
|
||||
outln(e);
|
||||
++nAbortedScript;
|
||||
}catch(Exception e){
|
||||
++nAbortedScript;
|
||||
throw e;
|
||||
}finally{
|
||||
outln("<<<<<----- ",nTest," test(s) in ",ts.getFilename());
|
||||
}
|
||||
outln("<<<<<----- ",nTest," test(s) in ",ts.getFilename());
|
||||
}
|
||||
Util.unlink(initialDbName);
|
||||
}
|
||||
@ -264,7 +233,7 @@ public class SQLTester {
|
||||
if( 0!=rc ){
|
||||
final String msg = sqlite3_errmsg(db);
|
||||
sqlite3_close(db);
|
||||
Util.toss(TestFailure.class, "db open failed with code ",
|
||||
Util.toss(SQLTesterException.class, "db open failed with code ",
|
||||
rc," and message: ",msg);
|
||||
}
|
||||
return aDb[iCurrentDb] = db;
|
||||
@ -364,70 +333,89 @@ public class SQLTester {
|
||||
final StringBuilder sb = (ResultBufferMode.NONE==appendMode)
|
||||
? null : resultBuffer;
|
||||
//outln("sqlChunk len= = ",sqlChunk.length);
|
||||
while(pos < sqlChunk.length){
|
||||
if(pos > 0){
|
||||
sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
|
||||
sqlChunk.length);
|
||||
}
|
||||
if( 0==sqlChunk.length ) break;
|
||||
rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
|
||||
/*outln("PREPARE rc ",rc," oTail=",oTail.getValue(),": ",
|
||||
new String(sqlChunk,StandardCharsets.UTF_8),"\n<EOSQL>");*/
|
||||
if( 0!=rc ){
|
||||
if(throwOnError){
|
||||
Util.toss(RuntimeException.class, "db op failed with rc="
|
||||
+rc+": "+sqlite3_errmsg(db));
|
||||
}else if( null!=sb ){
|
||||
appendDbErr(db, sb, rc);
|
||||
try{
|
||||
while(pos < sqlChunk.length){
|
||||
if(pos > 0){
|
||||
sqlChunk = Arrays.copyOfRange(sqlChunk, pos,
|
||||
sqlChunk.length);
|
||||
}
|
||||
break;
|
||||
}
|
||||
pos = oTail.getValue();
|
||||
stmt = outStmt.getValue();
|
||||
if( null == stmt ){
|
||||
// empty statement was parsed.
|
||||
continue;
|
||||
}
|
||||
if( null!=sb ){
|
||||
// Add the output to the result buffer...
|
||||
final int nCol = sqlite3_column_count(stmt);
|
||||
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
|
||||
for(int i = 0; i < nCol; ++i){
|
||||
if( spacing++ > 0 ) sb.append(' ');
|
||||
String val = sqlite3_column_text16(stmt, i);
|
||||
if( null==val ){
|
||||
sb.append( nullView );
|
||||
continue;
|
||||
}
|
||||
switch(appendMode){
|
||||
case ASIS:
|
||||
sb.append( val );
|
||||
break;
|
||||
case ESCAPED:
|
||||
sb.append( escapeSqlValue(val) );
|
||||
break;
|
||||
default:
|
||||
Util.toss(RuntimeException.class, "Unhandled ResultBufferMode.");
|
||||
}
|
||||
}
|
||||
if( ResultRowMode.NEWLINE == lineMode ){
|
||||
spacing = 0;
|
||||
sb.append('\n');
|
||||
if( 0==sqlChunk.length ) break;
|
||||
rc = sqlite3_prepare_v2(db, sqlChunk, outStmt, oTail);
|
||||
/*outln("PREPARE rc ",rc," oTail=",oTail.getValue(),": ",
|
||||
new String(sqlChunk,StandardCharsets.UTF_8),"\n<EOSQL>");*/
|
||||
if( 0!=rc ){
|
||||
if(throwOnError){
|
||||
Util.toss(RuntimeException.class, "db op failed with rc="
|
||||
+rc+": "+sqlite3_errmsg(db));
|
||||
}else if( null!=sb ){
|
||||
appendDbErr(db, sb, rc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
pos = oTail.getValue();
|
||||
stmt = outStmt.getValue();
|
||||
if( null == stmt ){
|
||||
// empty statement was parsed.
|
||||
continue;
|
||||
}
|
||||
}else{
|
||||
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
|
||||
else if( rc!=0 ){
|
||||
if( null!=sb ){
|
||||
appendDbErr(db, sb, rc);
|
||||
// Add the output to the result buffer...
|
||||
final int nCol = sqlite3_column_count(stmt);
|
||||
String colName = null, val = null;
|
||||
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){
|
||||
for(int i = 0; i < nCol; ++i){
|
||||
if( spacing++ > 0 ) sb.append(' ');
|
||||
if( emitColNames ){
|
||||
colName = sqlite3_column_name(stmt, i);
|
||||
switch(appendMode){
|
||||
case ASIS:
|
||||
sb.append( colName );
|
||||
break;
|
||||
case ESCAPED:
|
||||
sb.append( escapeSqlValue(colName) );
|
||||
break;
|
||||
default:
|
||||
Util.toss(RuntimeException.class, "Unhandled ResultBufferMode.");
|
||||
}
|
||||
sb.append(' ');
|
||||
}
|
||||
val = sqlite3_column_text16(stmt, i);
|
||||
if( null==val ){
|
||||
sb.append( nullView );
|
||||
continue;
|
||||
}
|
||||
switch(appendMode){
|
||||
case ASIS:
|
||||
sb.append( val );
|
||||
break;
|
||||
case ESCAPED:
|
||||
sb.append( escapeSqlValue(val) );
|
||||
break;
|
||||
default:
|
||||
Util.toss(RuntimeException.class, "Unhandled ResultBufferMode.");
|
||||
}
|
||||
}
|
||||
if( ResultRowMode.NEWLINE == lineMode ){
|
||||
spacing = 0;
|
||||
sb.append('\n');
|
||||
}
|
||||
}
|
||||
}else{
|
||||
while( SQLITE_ROW == (rc = sqlite3_step(stmt)) ){}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
stmt = null;
|
||||
if(SQLITE_ROW==rc || SQLITE_DONE==rc) rc = 0;
|
||||
else if( rc!=0 ){
|
||||
if( null!=sb ){
|
||||
appendDbErr(db, sb, rc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}finally{
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
if( 0!=rc && throwOnError ){
|
||||
Util.toss(RuntimeException.class, "db op failed with rc="
|
||||
+rc+": "+sqlite3_errmsg(db));
|
||||
@ -443,8 +431,6 @@ public class SQLTester {
|
||||
final String flag = a.replaceFirst("-+","");
|
||||
if( flag.equals("verbose") ){
|
||||
t.setVerbosity(t.getVerbosity() + 1);
|
||||
}else if( flag.equals("2") ){
|
||||
v2 = true;
|
||||
}else{
|
||||
throw new IllegalArgumentException("Unhandled flag: "+flag);
|
||||
}
|
||||
@ -452,11 +438,13 @@ public class SQLTester {
|
||||
}
|
||||
t.addTestScript(a);
|
||||
}
|
||||
if( v2 ) t.runTests2();
|
||||
else t.runTests();
|
||||
t.outln("Processed ",t.nTotalTest," test(s) in ",t.nTestFile," file(s).");
|
||||
if( t.nAbortedScript > 0 ){
|
||||
t.outln("Aborted ",t.nAbortedScript," script(s).");
|
||||
try {
|
||||
t.runTests();
|
||||
}finally{
|
||||
t.outln("Processed ",t.nTotalTest," test(s) in ",t.nTestFile," file(s).");
|
||||
if( t.nAbortedScript > 0 ){
|
||||
t.outln("Aborted ",t.nAbortedScript," script(s).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -492,363 +480,6 @@ public class SQLTester {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
Base class for test script commands. It provides a set of utility
|
||||
APIs for concrete command implementations.
|
||||
|
||||
Each subclass must have a public no-arg ctor and must implement
|
||||
the process() method which is abstract in this class.
|
||||
|
||||
Commands are intended to be stateless, except perhaps for counters
|
||||
and similar internals. No state which changes the behavior between
|
||||
any two invocations of process() should be retained.
|
||||
*/
|
||||
abstract class Command {
|
||||
protected Command(){}
|
||||
|
||||
/**
|
||||
Must process one command-unit of work and either return
|
||||
(on success) or throw (on error).
|
||||
|
||||
The first argument is the context of the test.
|
||||
|
||||
argv is a list with the command name followed by any arguments to
|
||||
that command. The argcCheck() method from this class provides
|
||||
very basic argc validation.
|
||||
|
||||
The content is any text content which was specified after the
|
||||
command, or null if there is null. Any command which does not
|
||||
permit content must pass that argument to affirmNoContent() in
|
||||
their constructor (or perform an equivalent check). Similary,
|
||||
those which require content must pass it to affirmHasContent()
|
||||
(or equivalent).
|
||||
*/
|
||||
public abstract void process(SQLTester tester, String[] argv, String content) throws Exception;
|
||||
|
||||
/**
|
||||
If argv.length-1 (-1 because the command's name is in argv[0]) does not
|
||||
fall in the inclusive range (min,max) then this function throws. Use
|
||||
a max value of -1 to mean unlimited.
|
||||
*/
|
||||
protected final void argcCheck(String[] argv, int min, int max) throws Exception{
|
||||
int argc = argv.length-1;
|
||||
if(argc<min || (max>=0 && argc>max)){
|
||||
if( min==max ){
|
||||
Util.badArg(argv[0]," requires exactly ",min," argument(s)");
|
||||
}else if(max>0){
|
||||
Util.badArg(argv[0]," requires ",min,"-",max," arguments.");
|
||||
}else{
|
||||
Util.badArg(argv[0]," requires at least ",min," arguments.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Equivalent to argcCheck(argv,argc,argc).
|
||||
*/
|
||||
protected final void argcCheck(String[] argv, int argc) throws Exception{
|
||||
argcCheck(argv, argc, argc);
|
||||
}
|
||||
|
||||
//! Throws if content is not null.
|
||||
protected void affirmNoContent(String content) throws Exception{
|
||||
if(null != content){
|
||||
Util.badArg(this.getClass().getName()," does not accept content ",
|
||||
"but got:\n",content);
|
||||
}
|
||||
}
|
||||
|
||||
//! Throws if content is null.
|
||||
protected void affirmHasContent(String content) throws Exception{
|
||||
if(null == content){
|
||||
Util.badArg(this.getClass().getName()," requires content.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CloseDbCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,0,1);
|
||||
affirmNoContent(content);
|
||||
Integer id;
|
||||
if(argv.length>1){
|
||||
String arg = argv[1];
|
||||
if("all".equals(arg)){
|
||||
//t.verbose(argv[0]," all dbs");
|
||||
t.closeAllDbs();
|
||||
return;
|
||||
}
|
||||
else{
|
||||
id = Integer.parseInt(arg);
|
||||
}
|
||||
}else{
|
||||
id = t.getCurrentDbId();
|
||||
}
|
||||
t.closeDb(id);
|
||||
t.verbose(argv[0]," db ",id);
|
||||
}
|
||||
}
|
||||
|
||||
//! --db command
|
||||
class DbCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
affirmNoContent(content);
|
||||
final sqlite3 db = t.setCurrentDb( Integer.parseInt(argv[1]) );
|
||||
//t.verbose(argv[0]," set db to ",db);
|
||||
}
|
||||
}
|
||||
|
||||
//! --glob command
|
||||
class GlobCommand extends Command {
|
||||
private boolean negate = false;
|
||||
public GlobCommand(){}
|
||||
protected GlobCommand(boolean negate){ this.negate = negate; }
|
||||
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
affirmNoContent(content);
|
||||
t.incrementTestCounter();
|
||||
final String sql = t.takeInputBuffer();
|
||||
//t.verbose(argv[0]," SQL =\n",sql);
|
||||
int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
|
||||
ResultRowMode.ONELINE, sql);
|
||||
final String result = t.getResultText();
|
||||
final String sArgs = Util.argvToString(argv);
|
||||
//t.verbose(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
|
||||
final String glob = argv[1];
|
||||
rc = SQLTester.strglob(glob, result);
|
||||
if( (negate && 0==rc) || (!negate && 0!=rc) ){
|
||||
Util.toss(TestFailure.class, argv[0], " mismatch: ",
|
||||
glob," vs input: ",result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --json command
|
||||
class JsonCommand extends ResultCommand {
|
||||
public JsonCommand(){ super(ResultBufferMode.ASIS); }
|
||||
}
|
||||
|
||||
//! --json-block command
|
||||
class JsonBlockCommand extends TableResultCommand {
|
||||
public JsonBlockCommand(){ super(true); }
|
||||
}
|
||||
|
||||
//! --new command
|
||||
class NewDbCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
affirmNoContent(content);
|
||||
String fname = argv[1];
|
||||
Util.unlink(fname);
|
||||
final sqlite3 db = t.openDb(fname, true);
|
||||
//t.verbose(argv[0]," db ",db);
|
||||
}
|
||||
}
|
||||
|
||||
//! Placeholder dummy/no-op commands
|
||||
class NoopCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
}
|
||||
}
|
||||
|
||||
//! --notglob command
|
||||
class NotGlobCommand extends GlobCommand {
|
||||
public NotGlobCommand(){
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
|
||||
//! --null command
|
||||
class NullCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
affirmNoContent(content);
|
||||
t.setNullValue(argv[1]);
|
||||
//t.verbose(argv[0]," ",argv[1]);
|
||||
}
|
||||
}
|
||||
|
||||
//! --open command
|
||||
class OpenDbCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
affirmNoContent(content);
|
||||
String fname = argv[1];
|
||||
final sqlite3 db = t.openDb(fname, false);
|
||||
//t.verbose(argv[0]," db ",db);
|
||||
}
|
||||
}
|
||||
|
||||
//! --print command
|
||||
class PrintCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
if( 1==argv.length && null==content ){
|
||||
Util.badArg(argv[0]," requires at least 1 argument or body content.");
|
||||
}
|
||||
if( argv.length > 1 ) t.outln("\t",Util.argvToString(argv));
|
||||
if( null!=content ) t.outln(content.replaceAll("(?m)^", "\t"));
|
||||
}
|
||||
}
|
||||
|
||||
//! --result command
|
||||
class ResultCommand extends Command {
|
||||
private final ResultBufferMode bufferMode;
|
||||
protected ResultCommand(ResultBufferMode bm){ bufferMode = bm; }
|
||||
public ResultCommand(){ this(ResultBufferMode.ESCAPED); }
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,0,-1);
|
||||
affirmNoContent(content);
|
||||
t.incrementTestCounter();
|
||||
final String sql = t.takeInputBuffer();
|
||||
//t.verbose(argv[0]," SQL =\n",sql);
|
||||
int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
|
||||
final String result = t.getResultText().trim();
|
||||
final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
|
||||
if( !result.equals(sArgs) ){
|
||||
t.outln(argv[0]," FAILED comparison. Result buffer:\n",
|
||||
result,"\nargs:\n",sArgs);
|
||||
Util.toss(TestFailure.class, argv[0]," comparison failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --run command
|
||||
class RunCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,0,1);
|
||||
affirmHasContent(content);
|
||||
final sqlite3 db = (1==argv.length)
|
||||
? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
|
||||
int rc = t.execSql(db, false, ResultBufferMode.NONE,
|
||||
ResultRowMode.ONELINE, content);
|
||||
if( 0!=rc && t.isVerbose() ){
|
||||
String msg = sqlite3_errmsg(db);
|
||||
t.verbose(argv[0]," non-fatal command error #",rc,": ",
|
||||
msg,"\nfor SQL:\n",content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --tableresult command
|
||||
class TableResultCommand extends Command {
|
||||
private final boolean jsonMode;
|
||||
protected TableResultCommand(boolean jsonMode){ this.jsonMode = jsonMode; }
|
||||
public TableResultCommand(){ this(false); }
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,0);
|
||||
affirmHasContent(content);
|
||||
t.incrementTestCounter();
|
||||
if( !content.endsWith("\n--end") ){
|
||||
Util.toss(TestFailure.class, argv[0], " must be terminated with --end.");
|
||||
}else{
|
||||
int n = content.length();
|
||||
content = content.substring(0, n-6);
|
||||
}
|
||||
final String[] globs = content.split("\\s*\\n\\s*");
|
||||
if( globs.length < 1 ){
|
||||
Util.toss(TestFailure.class, argv[0], " requires 1 or more ",
|
||||
(jsonMode ? "json snippets" : "globs"),".");
|
||||
}
|
||||
final String sql = t.takeInputBuffer();
|
||||
t.execSql(null, true,
|
||||
jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
|
||||
ResultRowMode.NEWLINE, sql);
|
||||
final String rbuf = t.getResultText();
|
||||
final String[] res = rbuf.split("\n");
|
||||
if( res.length != globs.length ){
|
||||
Util.toss(TestFailure.class, argv[0], " failure: input has ",
|
||||
res.length," row(s) but expecting ",globs.length);
|
||||
}
|
||||
for(int i = 0; i < res.length; ++i){
|
||||
final String glob = globs[i].replaceAll("\\s+"," ").trim();
|
||||
//t.verbose(argv[0]," <<",glob,">> vs <<",res[i],">>");
|
||||
if( jsonMode ){
|
||||
if( !glob.equals(res[i]) ){
|
||||
Util.toss(TestFailure.class, argv[0], " json <<",glob,
|
||||
">> does not match: <<",res[i],">>");
|
||||
}
|
||||
}else if( 0 != SQLTester.strglob(glob, res[i]) ){
|
||||
Util.toss(TestFailure.class, argv[0], " glob <<",glob,
|
||||
">> does not match: <<",res[i],">>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --testcase command
|
||||
class TestCaseCommand extends Command {
|
||||
public void process(SQLTester t, String[] argv, String content) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
affirmHasContent(content);
|
||||
// TODO: do something with the test name
|
||||
t.clearResultBuffer();
|
||||
t.clearInputBuffer().append(content);
|
||||
//t.verbose(argv[0]," input buffer: ",content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Helper for dispatching Command instances.
|
||||
*/
|
||||
class CommandDispatcher {
|
||||
|
||||
private static java.util.Map<String,Command> commandMap =
|
||||
new java.util.HashMap<>();
|
||||
|
||||
/**
|
||||
Returns a (cached) instance mapped to name, or null if no match
|
||||
is found.
|
||||
*/
|
||||
static Command getCommandByName(String name){
|
||||
Command rv = commandMap.get(name);
|
||||
if( null!=rv ) return rv;
|
||||
switch(name){
|
||||
case "close": rv = new CloseDbCommand(); break;
|
||||
case "db": rv = new DbCommand(); break;
|
||||
case "glob": rv = new GlobCommand(); break;
|
||||
case "json": rv = new JsonCommand(); break;
|
||||
case "json-block": rv = new JsonBlockCommand(); break;
|
||||
case "new": rv = new NewDbCommand(); break;
|
||||
case "notglob": rv = new NotGlobCommand(); break;
|
||||
case "null": rv = new NullCommand(); break;
|
||||
case "oom": rv = new NoopCommand(); break;
|
||||
case "open": rv = new OpenDbCommand(); break;
|
||||
case "print": rv = new PrintCommand(); break;
|
||||
case "result": rv = new ResultCommand(); break;
|
||||
case "run": rv = new RunCommand(); break;
|
||||
case "tableresult": rv = new TableResultCommand(); break;
|
||||
case "testcase": rv = new TestCaseCommand(); break;
|
||||
default: rv = null; break;
|
||||
}
|
||||
if( null!=rv ) commandMap.put(name, rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
Treats argv[0] as a command name, looks it up with
|
||||
getCommandByName(), and calls process() on that instance, passing
|
||||
it arguments given to this function.
|
||||
*/
|
||||
static void dispatch(SQLTester tester, String[] argv, String content) throws Exception{
|
||||
final Command cmd = getCommandByName(argv[0]);
|
||||
if(null == cmd){
|
||||
final TestScript ts = tester.getCurrentScript();
|
||||
if( tester.skipUnknownCommands() ){
|
||||
tester.outln("WARNING: skipping remainder of [",ts.getModuleName(),
|
||||
"] because it contains unknown command '",argv[0],"'.");
|
||||
throw new SkipTestRemainder(ts);
|
||||
}
|
||||
Util.toss(IllegalArgumentException.class,
|
||||
"No command handler found for '"+argv[0]+"' in ",
|
||||
ts.getName());
|
||||
}
|
||||
//tester.verbose("Running ",argv[0]," with:\n", content);
|
||||
cmd.process(tester, argv, content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
General utilities for the SQLTester bits.
|
||||
*/
|
||||
|
@ -12,29 +12,384 @@
|
||||
** This file contains the TestScript part of the SQLTester framework.
|
||||
*/
|
||||
package org.sqlite.jni.tester;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.io.*;
|
||||
import static org.sqlite.jni.SQLite3Jni.*;
|
||||
import org.sqlite.jni.sqlite3;
|
||||
import java.util.Arrays;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.*;
|
||||
|
||||
class TestScriptFailed extends SQLTesterException {
|
||||
public TestScriptFailed(TestScript ts, String msg){
|
||||
super(ts.getOutputPrefix()+": "+msg);
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownCommand extends SQLTesterException {
|
||||
public UnknownCommand(TestScript ts, String cmd){
|
||||
super(ts.getOutputPrefix()+": unknown command: "+cmd);
|
||||
}
|
||||
}
|
||||
|
||||
class IncompatibleDirective extends SQLTesterException {
|
||||
public IncompatibleDirective(TestScript ts, String line){
|
||||
super(ts.getOutputPrefix()+": incompatible directive: "+line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Base class for test script commands. It provides a set of utility
|
||||
APIs for concrete command implementations.
|
||||
|
||||
Each subclass must have a public no-arg ctor and must implement
|
||||
the process() method which is abstract in this class.
|
||||
|
||||
Commands are intended to be stateless, except perhaps for counters
|
||||
and similar internals. Specifically, no state which changes the
|
||||
behavior between any two invocations of process() should be
|
||||
retained.
|
||||
*/
|
||||
abstract class Command {
|
||||
protected Command(){}
|
||||
|
||||
/**
|
||||
Must process one command-unit of work and either return
|
||||
(on success) or throw (on error).
|
||||
|
||||
The first two arguments specify the context of the test.
|
||||
|
||||
argv is a list with the command name followed by any arguments to
|
||||
that command. The argcCheck() method from this class provides
|
||||
very basic argc validation.
|
||||
*/
|
||||
public abstract void process(
|
||||
SQLTester st, TestScript ts, String[] argv
|
||||
) throws Exception;
|
||||
|
||||
/**
|
||||
If argv.length-1 (-1 because the command's name is in argv[0]) does not
|
||||
fall in the inclusive range (min,max) then this function throws. Use
|
||||
a max value of -1 to mean unlimited.
|
||||
*/
|
||||
protected final void argcCheck(TestScript ts, String[] argv, int min, int max) throws Exception{
|
||||
int argc = argv.length-1;
|
||||
if(argc<min || (max>=0 && argc>max)){
|
||||
if( min==max ){
|
||||
ts.toss(argv[0]," requires exactly ",min," argument(s)");
|
||||
}else if(max>0){
|
||||
ts.toss(argv[0]," requires ",min,"-",max," arguments.");
|
||||
}else{
|
||||
ts.toss(argv[0]," requires at least ",min," arguments.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Equivalent to argcCheck(argv,argc,argc).
|
||||
*/
|
||||
protected final void argcCheck(TestScript ts, String[] argv, int argc) throws Exception{
|
||||
argcCheck(ts, argv, argc, argc);
|
||||
}
|
||||
}
|
||||
|
||||
//! --close command
|
||||
class CloseDbCommand extends Command {
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,0,1);
|
||||
Integer id;
|
||||
if(argv.length>1){
|
||||
String arg = argv[1];
|
||||
if("all".equals(arg)){
|
||||
t.closeAllDbs();
|
||||
return;
|
||||
}
|
||||
else{
|
||||
id = Integer.parseInt(arg);
|
||||
}
|
||||
}else{
|
||||
id = t.getCurrentDbId();
|
||||
}
|
||||
t.closeDb(id);
|
||||
}
|
||||
}
|
||||
|
||||
//! --column-names command
|
||||
class ColumnNamesCommand extends Command {
|
||||
public void process(
|
||||
SQLTester st, TestScript ts, String[] argv
|
||||
) throws Exception{
|
||||
argcCheck(ts,argv,1);
|
||||
st.outputColumnNames( Integer.parseInt(argv[1])!=0 );
|
||||
}
|
||||
}
|
||||
|
||||
//! --db command
|
||||
class DbCommand extends Command {
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,1);
|
||||
t.setCurrentDb( Integer.parseInt(argv[1]) );
|
||||
}
|
||||
}
|
||||
|
||||
//! --glob command
|
||||
class GlobCommand extends Command {
|
||||
private boolean negate = false;
|
||||
public GlobCommand(){}
|
||||
protected GlobCommand(boolean negate){ this.negate = negate; }
|
||||
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,1);
|
||||
t.incrementTestCounter();
|
||||
final String sql = t.takeInputBuffer();
|
||||
int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
|
||||
ResultRowMode.ONELINE, sql);
|
||||
final String result = t.getResultText();
|
||||
final String sArgs = Util.argvToString(argv);
|
||||
//t.verbose(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
|
||||
final String glob = argv[1];
|
||||
rc = SQLTester.strglob(glob, result);
|
||||
if( (negate && 0==rc) || (!negate && 0!=rc) ){
|
||||
ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --json command
|
||||
class JsonCommand extends ResultCommand {
|
||||
public JsonCommand(){ super(ResultBufferMode.ASIS); }
|
||||
}
|
||||
|
||||
//! --json-block command
|
||||
class JsonBlockCommand extends TableResultCommand {
|
||||
public JsonBlockCommand(){ super(true); }
|
||||
}
|
||||
|
||||
//! --new command
|
||||
class NewDbCommand extends OpenDbCommand {
|
||||
public NewDbCommand(){ super(true); }
|
||||
}
|
||||
|
||||
//! Placeholder dummy/no-op commands
|
||||
class NoopCommand extends Command {
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
}
|
||||
}
|
||||
|
||||
//! --notglob command
|
||||
class NotGlobCommand extends GlobCommand {
|
||||
public NotGlobCommand(){
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
|
||||
//! --null command
|
||||
class NullCommand extends Command {
|
||||
public void process(
|
||||
SQLTester st, TestScript ts, String[] argv
|
||||
) throws Exception{
|
||||
argcCheck(ts,argv,1);
|
||||
st.setNullValue( argv[1] );
|
||||
}
|
||||
}
|
||||
|
||||
//! --open command
|
||||
class OpenDbCommand extends Command {
|
||||
private boolean createIfNeeded = false;
|
||||
public OpenDbCommand(){}
|
||||
protected OpenDbCommand(boolean c){createIfNeeded = c;}
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,1);
|
||||
t.openDb(argv[1], createIfNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
//! --print command
|
||||
class PrintCommand extends Command {
|
||||
public void process(
|
||||
SQLTester st, TestScript ts, String[] argv
|
||||
) throws Exception{
|
||||
st.out(ts.getOutputPrefix(),": ");
|
||||
final String body = ts.fetchCommandBody();
|
||||
if( 1==argv.length && null==body ){
|
||||
st.out( st.getInputText() );
|
||||
}else{
|
||||
st.outln( Util.argvToString(argv) );
|
||||
}
|
||||
if( null!=body ){
|
||||
st.out(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --result command
|
||||
class ResultCommand extends Command {
|
||||
private final ResultBufferMode bufferMode;
|
||||
protected ResultCommand(ResultBufferMode bm){ bufferMode = bm; }
|
||||
public ResultCommand(){ this(ResultBufferMode.ESCAPED); }
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,0,-1);
|
||||
t.incrementTestCounter();
|
||||
final String sql = t.takeInputBuffer();
|
||||
//t.verbose(argv[0]," SQL =\n",sql);
|
||||
int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
|
||||
final String result = t.getResultText().trim();
|
||||
final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
|
||||
if( !result.equals(sArgs) ){
|
||||
t.outln(argv[0]," FAILED comparison. Result buffer:\n",
|
||||
result,"\nargs:\n",sArgs);
|
||||
ts.toss(argv[0]+" comparison failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --run command
|
||||
class RunCommand extends Command {
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,0,1);
|
||||
final sqlite3 db = (1==argv.length)
|
||||
? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
|
||||
final String sql = t.takeInputBuffer();
|
||||
int rc = t.execSql(db, false, ResultBufferMode.NONE,
|
||||
ResultRowMode.ONELINE, sql);
|
||||
if( 0!=rc && t.isVerbose() ){
|
||||
String msg = sqlite3_errmsg(db);
|
||||
t.verbose(argv[0]," non-fatal command error #",rc,": ",
|
||||
msg,"\nfor SQL:\n",sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --tableresult command
|
||||
class TableResultCommand extends Command {
|
||||
private final boolean jsonMode;
|
||||
protected TableResultCommand(boolean jsonMode){ this.jsonMode = jsonMode; }
|
||||
public TableResultCommand(){ this(false); }
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,0);
|
||||
t.incrementTestCounter();
|
||||
String body = ts.fetchCommandBody();
|
||||
if( null==body ) ts.toss("Missing ",argv[0]," body.");
|
||||
body = body.trim();
|
||||
if( !body.endsWith("\n--end") ){
|
||||
ts.toss(argv[0], " must be terminated with --end.");
|
||||
}else{
|
||||
int n = body.length();
|
||||
body = body.substring(0, n-6);
|
||||
}
|
||||
final String[] globs = body.split("\\s*\\n\\s*");
|
||||
if( globs.length < 1 ){
|
||||
ts.toss(argv[0], " requires 1 or more ",
|
||||
(jsonMode ? "json snippets" : "globs"),".");
|
||||
}
|
||||
final String sql = t.takeInputBuffer();
|
||||
t.execSql(null, true,
|
||||
jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
|
||||
ResultRowMode.NEWLINE, sql);
|
||||
final String rbuf = t.getResultText();
|
||||
final String[] res = rbuf.split("\n");
|
||||
if( res.length != globs.length ){
|
||||
ts.toss(argv[0], " failure: input has ", res.length,
|
||||
" row(s) but expecting ",globs.length);
|
||||
}
|
||||
for(int i = 0; i < res.length; ++i){
|
||||
final String glob = globs[i].replaceAll("\\s+"," ").trim();
|
||||
//t.verbose(argv[0]," <<",glob,">> vs <<",res[i],">>");
|
||||
if( jsonMode ){
|
||||
if( !glob.equals(res[i]) ){
|
||||
ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
|
||||
res[i],">>");
|
||||
}
|
||||
}else if( 0 != SQLTester.strglob(glob, res[i]) ){
|
||||
ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --testcase command
|
||||
class TestCaseCommand extends Command {
|
||||
public void process(SQLTester t, TestScript ts, String[] argv) throws Exception{
|
||||
argcCheck(ts,argv,1);
|
||||
// TODO?: do something with the test name
|
||||
t.clearResultBuffer();
|
||||
t.clearInputBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
class CommandDispatcher2 {
|
||||
|
||||
private static java.util.Map<String,Command> commandMap =
|
||||
new java.util.HashMap<>();
|
||||
|
||||
/**
|
||||
Returns a (cached) instance mapped to name, or null if no match
|
||||
is found.
|
||||
*/
|
||||
static Command getCommandByName(String name){
|
||||
Command rv = commandMap.get(name);
|
||||
if( null!=rv ) return rv;
|
||||
switch(name){
|
||||
case "close": rv = new CloseDbCommand(); break;
|
||||
case "column-names":rv = new ColumnNamesCommand(); break;
|
||||
case "db": rv = new DbCommand(); break;
|
||||
case "glob": rv = new GlobCommand(); break;
|
||||
case "json": rv = new JsonCommand(); break;
|
||||
case "json-block": rv = new JsonBlockCommand(); break;
|
||||
case "new": rv = new NewDbCommand(); break;
|
||||
case "notglob": rv = new NotGlobCommand(); break;
|
||||
case "null": rv = new NullCommand(); break;
|
||||
case "oom": rv = new NoopCommand(); break;
|
||||
case "open": rv = new OpenDbCommand(); break;
|
||||
case "print": rv = new PrintCommand(); break;
|
||||
case "result": rv = new ResultCommand(); break;
|
||||
case "run": rv = new RunCommand(); break;
|
||||
case "tableresult": rv = new TableResultCommand(); break;
|
||||
case "testcase": rv = new TestCaseCommand(); break;
|
||||
default: rv = null; break;
|
||||
}
|
||||
if( null!=rv ) commandMap.put(name, rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
Treats argv[0] as a command name, looks it up with
|
||||
getCommandByName(), and calls process() on that instance, passing
|
||||
it arguments given to this function.
|
||||
*/
|
||||
static void dispatch(SQLTester tester, TestScript ts, String[] argv) throws Exception{
|
||||
final Command cmd = getCommandByName(argv[0]);
|
||||
if(null == cmd){
|
||||
throw new UnknownCommand(ts, argv[0]);
|
||||
}
|
||||
cmd.process(tester, ts, argv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
This class represents a single test script. It handles (or
|
||||
delegates) its the reading-in and parsing, but the details of
|
||||
evaluation are delegated elsewhere.
|
||||
*/
|
||||
class TestScript {
|
||||
private String name = null;
|
||||
private String filename = null;
|
||||
private String moduleName = null;
|
||||
private List<CommandChunk> chunks = null;
|
||||
private final Cursor cur = new Cursor();
|
||||
private final Outer outer = new Outer();
|
||||
private String ignoreReason = null;
|
||||
private byte[] baScript = null;
|
||||
|
||||
/* One "chunk" of input, representing a single command and
|
||||
its optional body content. */
|
||||
private static final class CommandChunk {
|
||||
public String[] argv = null;
|
||||
public String content = null;
|
||||
private static final class Cursor {
|
||||
private final StringBuilder sb = new StringBuilder();
|
||||
byte[] src = null;
|
||||
int pos = 0;
|
||||
int putbackPos = 0;
|
||||
int putbackLineNo = 0;
|
||||
int lineNo = 0 /* yes, zero */;
|
||||
int peekedPos = 0;
|
||||
int peekedLineNo = 0;
|
||||
boolean inComment = false;
|
||||
|
||||
void reset(){
|
||||
sb.setLength(0); pos = 0; lineNo = 0/*yes, zero*/; inComment = false;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readFile(String filename) throws Exception {
|
||||
@ -43,229 +398,289 @@ class TestScript {
|
||||
|
||||
/**
|
||||
Initializes the script with the content of the given file.
|
||||
Throws if it cannot read the file or if tokenizing it fails.
|
||||
Throws if it cannot read the file.
|
||||
*/
|
||||
public TestScript(String filename) throws Exception{
|
||||
name = filename;
|
||||
baScript = readFile(filename);
|
||||
setContent(new String(
|
||||
baScript, java.nio.charset.StandardCharsets.UTF_8
|
||||
));
|
||||
this.filename = filename;
|
||||
setVerbosity(2);
|
||||
cur.src = readFile(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
Initializes the script with the given content, copied at
|
||||
construction-time. The first argument is a filename for that
|
||||
content. It need not refer to a real file - it's for display
|
||||
purposes only.
|
||||
*/
|
||||
public TestScript(String virtualName, StringBuffer content)
|
||||
throws RuntimeException {
|
||||
name = virtualName;
|
||||
setContent(content.toString());
|
||||
}
|
||||
|
||||
private void setContent(String c){
|
||||
this.chunks = chunkContent(c);
|
||||
}
|
||||
|
||||
public String getName(){
|
||||
return name;
|
||||
public String getFilename(){
|
||||
return filename;
|
||||
}
|
||||
|
||||
public String getModuleName(){
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
public boolean isIgnored(){
|
||||
return null!=ignoreReason;
|
||||
}
|
||||
|
||||
public String getIgnoredReason(){
|
||||
return ignoreReason;
|
||||
}
|
||||
|
||||
public void setVerbosity(int level){
|
||||
outer.setVerbosity(level);
|
||||
}
|
||||
|
||||
public String getOutputPrefix(){
|
||||
return "["+(moduleName==null ? filename : moduleName)+"] line "+
|
||||
cur.lineNo;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
private TestScript verbose(Object... vals){
|
||||
outer.verbose(vals);
|
||||
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 static final Pattern patternHashLine =
|
||||
Pattern.compile("^#", Pattern.MULTILINE);
|
||||
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*)\\n");
|
||||
/**
|
||||
Returns true if the given script content should be ignored
|
||||
(because it contains certain content which indicates such).
|
||||
*/
|
||||
private boolean shouldBeIgnored(String content){
|
||||
if( null == moduleName ){
|
||||
ignoreReason = "No module name.";
|
||||
return true;
|
||||
}else if( content.indexOf("\n|")>=0 ){
|
||||
ignoreReason = "Contains newline-pipe combination.";
|
||||
return true;
|
||||
}else if( content.indexOf(" MODULE_NAME:")>=0 ){
|
||||
ignoreReason = "Contains MODULE_NAME.";
|
||||
return true;
|
||||
}else if( content.indexOf("MIXED_MODULE_NAME:")>=0 ){
|
||||
ignoreReason = "Contains MIXED_MODULE_NAME.";
|
||||
return true;
|
||||
}
|
||||
Matcher m = patternHashLine.matcher(content);
|
||||
if( m.find() ){
|
||||
ignoreReason = "C-preprocessor line found.";
|
||||
return true;
|
||||
}
|
||||
m = patternRequiredProperties.matcher(content);
|
||||
if( m.find() ){
|
||||
ignoreReason = "REQUIRED_PROPERTIES found: "+m.group(1).trim();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean findModuleName(String content){
|
||||
final Pattern p = Pattern.compile(
|
||||
"SCRIPT_MODULE_NAME:\\s+(\\S+)\\s*\n",
|
||||
Pattern.MULTILINE
|
||||
);
|
||||
final Matcher m = p.matcher(content);
|
||||
moduleName = m.find() ? m.group(1) : null;
|
||||
return moduleName != null;
|
||||
}
|
||||
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-]+)( .*)?)$");
|
||||
|
||||
/**
|
||||
Chop script up into chunks containing individual commands and
|
||||
their inputs. The approach taken here is not as robust as
|
||||
line-by-line parsing would be but the framework is structured
|
||||
such that we could replace this part without unduly affecting the
|
||||
evaluation bits. The potential problems with this approach
|
||||
include:
|
||||
|
||||
- It's potentially possible that it will strip content out of a
|
||||
testcase block.
|
||||
|
||||
- It loses all file location information, so we can't report line
|
||||
numbers of errors.
|
||||
|
||||
If/when that becomes a problem, it can be refactored.
|
||||
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 List<CommandChunk> chunkContent(String content){
|
||||
findModuleName(content);
|
||||
if( shouldBeIgnored(content) ){
|
||||
chunks = null;
|
||||
return null;
|
||||
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;
|
||||
}
|
||||
|
||||
// First, strip out any content which we know we can ignore...
|
||||
final String sCComment = "[/][*]([*](?![/])|[^*])*[*][/]";
|
||||
final String s3Dash = "^---+[^\\n]*\\n";
|
||||
final String sEmptyLine = "^\\n";
|
||||
final String sOom = "^--oom\\n"
|
||||
/* Workaround: --oom is a top-level command in some contexts
|
||||
and appears in --testcase blocks in others. We don't
|
||||
do anything with --oom commands aside from ignore them, so
|
||||
elide them all to fix the --testcase blocks which contain
|
||||
them. */;
|
||||
final List<String> lPats = new ArrayList<>();
|
||||
lPats.add(sCComment);
|
||||
lPats.add(s3Dash);
|
||||
lPats.add(sEmptyLine);
|
||||
lPats.add(sOom);
|
||||
//verbose("Content:").verbose(content).verbose("<EOF>");
|
||||
for( String s : lPats ){
|
||||
final Pattern p = Pattern.compile(
|
||||
s, Pattern.MULTILINE
|
||||
);
|
||||
final Matcher m = p.matcher(content);
|
||||
/*verbose("Pattern {{{ ",p.pattern()," }}} with flags ",
|
||||
p.flags()," matches:"
|
||||
);*/
|
||||
int n = 0;
|
||||
//while( m.find() ) verbose("#",(++n),"\t",m.group(0).trim());
|
||||
content = m.replaceAll("");
|
||||
}
|
||||
// Chunk the newly-cleaned text into individual commands and their input...
|
||||
// First split up the input into command-size blocks...
|
||||
final List<String> blocks = new ArrayList<>();
|
||||
final Pattern p = Pattern.compile(
|
||||
"^--(?!end)[a-z]+", Pattern.MULTILINE
|
||||
// --end is a marker used by --tableresult and --(not)glob.
|
||||
);
|
||||
final Matcher m = p.matcher(content);
|
||||
int ndxPrev = 0, pos = 0, i = 0;
|
||||
//verbose("Trimmed content:").verbose(content).verbose("<EOF>");
|
||||
while( m.find() ){
|
||||
pos = m.start();
|
||||
final String block = content.substring(ndxPrev, pos).trim();
|
||||
if( 0==ndxPrev && pos>ndxPrev ){
|
||||
/* Initial block of non-command state. Skip it. */
|
||||
ndxPrev = pos + 2;
|
||||
continue;
|
||||
}
|
||||
if( !block.isEmpty() ){
|
||||
++i;
|
||||
//verbose("BLOCK #",i," ",+ndxPrev,"..",pos,block);
|
||||
blocks.add( block );
|
||||
}
|
||||
ndxPrev = pos + 2;
|
||||
}
|
||||
if( ndxPrev < content.length() ){
|
||||
// This all belongs to the final command
|
||||
final String block = content.substring(ndxPrev, content.length()).trim();
|
||||
if( !block.isEmpty() ){
|
||||
++i;
|
||||
//verbose("BLOCK #",(++i)," ",block);
|
||||
blocks.add( block );
|
||||
}
|
||||
}
|
||||
// Next, convert those blocks into higher-level CommandChunks...
|
||||
final List<CommandChunk> rc = new ArrayList<>();
|
||||
for( String block : blocks ){
|
||||
final CommandChunk chunk = new CommandChunk();
|
||||
final String[] parts = block.split("\\n", 2);
|
||||
chunk.argv = parts[0].split("\\s+");
|
||||
if( parts.length>1 && parts[1].length()>0 ){
|
||||
chunk.content = parts[1]
|
||||
/* reminder: don't trim() here. It would be easier
|
||||
for Command impls if we did but it makes debug
|
||||
output look weird. */;
|
||||
}
|
||||
rc.add( chunk );
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
Runs this test script in the context of the given tester object.
|
||||
If line looks like a command, returns an argv for that command
|
||||
invocation, else returns null.
|
||||
*/
|
||||
public void run(SQLTester tester) throws Exception {
|
||||
final int verbosity = tester.getVerbosity();
|
||||
if( null==chunks ){
|
||||
outer.outln("This test contains content which forces it to be skipped.");
|
||||
}else{
|
||||
int n = 0;
|
||||
for(CommandChunk chunk : chunks){
|
||||
if(verbosity>0){
|
||||
outer.out("VERBOSE",(verbosity>1 ? "+ " : " "),moduleName,
|
||||
" #",++n," ",chunk.argv[0],
|
||||
" ",Util.argvToString(chunk.argv));
|
||||
if(verbosity>1 && null!=chunk.content){
|
||||
outer.out("\n", chunk.content);
|
||||
}
|
||||
outer.out("\n");
|
||||
}
|
||||
CommandDispatcher.dispatch(
|
||||
tester, chunk.argv,
|
||||
(null==chunk.content) ? null : chunk.content.trim()
|
||||
);
|
||||
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,680 +0,0 @@
|
||||
/*
|
||||
** 2023-08-08
|
||||
**
|
||||
** The author disclaims copyright to this source code. In place of
|
||||
** a legal notice, here is a blessing:
|
||||
**
|
||||
** May you do good and not evil.
|
||||
** May you find forgiveness for yourself and forgive others.
|
||||
** May you share freely, never taking more than you give.
|
||||
**
|
||||
*************************************************************************
|
||||
** This file contains the TestScript2 part of the SQLTester framework.
|
||||
*/
|
||||
package org.sqlite.jni.tester;
|
||||
import static org.sqlite.jni.SQLite3Jni.*;
|
||||
import org.sqlite.jni.sqlite3;
|
||||
import java.util.Arrays;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.*;
|
||||
|
||||
class SQLTestException extends RuntimeException {
|
||||
public SQLTestException(String msg){
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class TestScript2Failed extends SQLTestException {
|
||||
public TestScript2Failed(TestScript2 ts, String msg){
|
||||
super(ts.getOutputPrefix()+": "+msg);
|
||||
}
|
||||
}
|
||||
|
||||
class SkipTestRemainder2 extends SQLTestException {
|
||||
public SkipTestRemainder2(TestScript2 ts){
|
||||
super(ts.getOutputPrefix()+": skipping remainder");
|
||||
}
|
||||
}
|
||||
|
||||
class IncompatibleDirective extends SQLTestException {
|
||||
public IncompatibleDirective(TestScript2 ts, String line){
|
||||
super(ts.getOutputPrefix()+": incompatible directive: "+line);
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownCommand extends SQLTestException {
|
||||
public UnknownCommand(TestScript2 ts, String line){
|
||||
super(ts.getOutputPrefix()+": unknown command: "+line);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Command2 {
|
||||
protected Command2(){}
|
||||
|
||||
public abstract void process(
|
||||
SQLTester st, TestScript2 ts, String[] argv
|
||||
) throws Exception;
|
||||
|
||||
/**
|
||||
If argv.length-1 (-1 because the command's name is in argv[0]) does not
|
||||
fall in the inclusive range (min,max) then this function throws. Use
|
||||
a max value of -1 to mean unlimited.
|
||||
*/
|
||||
protected final void argcCheck(String[] argv, int min, int max) throws Exception{
|
||||
int argc = argv.length-1;
|
||||
if(argc<min || (max>=0 && argc>max)){
|
||||
if( min==max ){
|
||||
Util.badArg(argv[0]," requires exactly ",min," argument(s)");
|
||||
}else if(max>0){
|
||||
Util.badArg(argv[0]," requires ",min,"-",max," arguments.");
|
||||
}else{
|
||||
Util.badArg(argv[0]," requires at least ",min," arguments.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Equivalent to argcCheck(argv,argc,argc).
|
||||
*/
|
||||
protected final void argcCheck(String[] argv, int argc) throws Exception{
|
||||
argcCheck(argv, argc, argc);
|
||||
}
|
||||
}
|
||||
|
||||
//! --close command
|
||||
class CloseDbCommand2 extends Command2 {
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,0,1);
|
||||
Integer id;
|
||||
if(argv.length>1){
|
||||
String arg = argv[1];
|
||||
if("all".equals(arg)){
|
||||
t.closeAllDbs();
|
||||
return;
|
||||
}
|
||||
else{
|
||||
id = Integer.parseInt(arg);
|
||||
}
|
||||
}else{
|
||||
id = t.getCurrentDbId();
|
||||
}
|
||||
t.closeDb(id);
|
||||
}
|
||||
}
|
||||
|
||||
//! --db command
|
||||
class DbCommand2 extends Command2 {
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
t.setCurrentDb( Integer.parseInt(argv[1]) );
|
||||
}
|
||||
}
|
||||
|
||||
//! --glob command
|
||||
class GlobCommand2 extends Command2 {
|
||||
private boolean negate = false;
|
||||
public GlobCommand2(){}
|
||||
protected GlobCommand2(boolean negate){ this.negate = negate; }
|
||||
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
t.incrementTestCounter();
|
||||
final String sql = t.takeInputBuffer();
|
||||
int rc = t.execSql(null, true, ResultBufferMode.ESCAPED,
|
||||
ResultRowMode.ONELINE, sql);
|
||||
final String result = t.getResultText();
|
||||
final String sArgs = Util.argvToString(argv);
|
||||
//t.verbose(argv[0]," rc = ",rc," result buffer:\n", result,"\nargs:\n",sArgs);
|
||||
final String glob = argv[1];
|
||||
rc = SQLTester.strglob(glob, result);
|
||||
if( (negate && 0==rc) || (!negate && 0!=rc) ){
|
||||
ts.toss(argv[0], " mismatch: ", glob," vs input: ",result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --json command
|
||||
class JsonCommand2 extends ResultCommand2 {
|
||||
public JsonCommand2(){ super(ResultBufferMode.ASIS); }
|
||||
}
|
||||
|
||||
//! --json-block command
|
||||
class JsonBlockCommand2 extends TableResultCommand2 {
|
||||
public JsonBlockCommand2(){ super(true); }
|
||||
}
|
||||
|
||||
//! --new command
|
||||
class NewDbCommand2 extends OpenDbCommand2 {
|
||||
public NewDbCommand2(){ super(true); }
|
||||
}
|
||||
|
||||
//! Placeholder dummy/no-op commands
|
||||
class NoopCommand2 extends Command2 {
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
}
|
||||
}
|
||||
|
||||
//! --notglob command
|
||||
class NotGlobCommand2 extends GlobCommand2 {
|
||||
public NotGlobCommand2(){
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
|
||||
//! --null command
|
||||
class NullCommand2 extends Command2 {
|
||||
public void process(
|
||||
SQLTester st, TestScript2 ts, String[] argv
|
||||
) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
st.setNullValue( argv[1] );
|
||||
}
|
||||
}
|
||||
|
||||
//! --open command
|
||||
class OpenDbCommand2 extends Command2 {
|
||||
private boolean createIfNeeded = false;
|
||||
public OpenDbCommand2(){}
|
||||
protected OpenDbCommand2(boolean c){createIfNeeded = c;}
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
t.openDb(argv[1], createIfNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
//! --print command
|
||||
class PrintCommand2 extends Command2 {
|
||||
public void process(
|
||||
SQLTester st, TestScript2 ts, String[] argv
|
||||
) throws Exception{
|
||||
st.out(ts.getOutputPrefix(),": ");
|
||||
final String body = ts.fetchCommandBody();
|
||||
if( 1==argv.length && null==body ){
|
||||
st.out( st.getInputText() );
|
||||
}else{
|
||||
st.outln( Util.argvToString(argv) );
|
||||
}
|
||||
if( null!=body ){
|
||||
st.out(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --result command
|
||||
class ResultCommand2 extends Command2 {
|
||||
private final ResultBufferMode bufferMode;
|
||||
protected ResultCommand2(ResultBufferMode bm){ bufferMode = bm; }
|
||||
public ResultCommand2(){ this(ResultBufferMode.ESCAPED); }
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,0,-1);
|
||||
t.incrementTestCounter();
|
||||
final String sql = t.takeInputBuffer();
|
||||
//t.verbose(argv[0]," SQL =\n",sql);
|
||||
int rc = t.execSql(null, false, bufferMode, ResultRowMode.ONELINE, sql);
|
||||
final String result = t.getResultText().trim();
|
||||
final String sArgs = argv.length>1 ? Util.argvToString(argv) : "";
|
||||
if( !result.equals(sArgs) ){
|
||||
t.outln(argv[0]," FAILED comparison. Result buffer:\n",
|
||||
result,"\nargs:\n",sArgs);
|
||||
ts.toss(argv[0]+" comparison failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --run command
|
||||
class RunCommand2 extends Command2 {
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,0,1);
|
||||
final sqlite3 db = (1==argv.length)
|
||||
? t.getCurrentDb() : t.getDbById( Integer.parseInt(argv[1]) );
|
||||
final String sql = t.takeInputBuffer();
|
||||
int rc = t.execSql(db, false, ResultBufferMode.NONE,
|
||||
ResultRowMode.ONELINE, sql);
|
||||
if( 0!=rc && t.isVerbose() ){
|
||||
String msg = sqlite3_errmsg(db);
|
||||
t.verbose(argv[0]," non-fatal command error #",rc,": ",
|
||||
msg,"\nfor SQL:\n",sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --tableresult command
|
||||
class TableResultCommand2 extends Command2 {
|
||||
private final boolean jsonMode;
|
||||
protected TableResultCommand2(boolean jsonMode){ this.jsonMode = jsonMode; }
|
||||
public TableResultCommand2(){ this(false); }
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,0);
|
||||
t.incrementTestCounter();
|
||||
String body = ts.fetchCommandBody();
|
||||
if( null==body ) ts.toss("Missing ",argv[0]," body.");
|
||||
body = body.trim();
|
||||
if( !body.endsWith("\n--end") ){
|
||||
ts.toss(argv[0], " must be terminated with --end.");
|
||||
}else{
|
||||
int n = body.length();
|
||||
body = body.substring(0, n-6);
|
||||
}
|
||||
final String[] globs = body.split("\\s*\\n\\s*");
|
||||
if( globs.length < 1 ){
|
||||
ts.toss(argv[0], " requires 1 or more ",
|
||||
(jsonMode ? "json snippets" : "globs"),".");
|
||||
}
|
||||
final String sql = t.takeInputBuffer();
|
||||
t.execSql(null, true,
|
||||
jsonMode ? ResultBufferMode.ASIS : ResultBufferMode.ESCAPED,
|
||||
ResultRowMode.NEWLINE, sql);
|
||||
final String rbuf = t.getResultText();
|
||||
final String[] res = rbuf.split("\n");
|
||||
if( res.length != globs.length ){
|
||||
ts.toss(argv[0], " failure: input has ", res.length,
|
||||
" row(s) but expecting ",globs.length);
|
||||
}
|
||||
for(int i = 0; i < res.length; ++i){
|
||||
final String glob = globs[i].replaceAll("\\s+"," ").trim();
|
||||
//t.verbose(argv[0]," <<",glob,">> vs <<",res[i],">>");
|
||||
if( jsonMode ){
|
||||
if( !glob.equals(res[i]) ){
|
||||
ts.toss(argv[0], " json <<",glob, ">> does not match: <<",
|
||||
res[i],">>");
|
||||
}
|
||||
}else if( 0 != SQLTester.strglob(glob, res[i]) ){
|
||||
ts.toss(argv[0], " glob <<",glob,">> does not match: <<",res[i],">>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//! --testcase command
|
||||
class TestCaseCommand2 extends Command2 {
|
||||
public void process(SQLTester t, TestScript2 ts, String[] argv) throws Exception{
|
||||
argcCheck(argv,1);
|
||||
// TODO?: do something with the test name
|
||||
final String body = ts.fetchCommandBody();
|
||||
t.clearResultBuffer();
|
||||
t.clearInputBuffer().append(null==body ? "" : body);
|
||||
}
|
||||
}
|
||||
|
||||
class CommandDispatcher2 {
|
||||
|
||||
private static java.util.Map<String,Command2> commandMap =
|
||||
new java.util.HashMap<>();
|
||||
|
||||
/**
|
||||
Returns a (cached) instance mapped to name, or null if no match
|
||||
is found.
|
||||
*/
|
||||
static Command2 getCommandByName(String name){
|
||||
Command2 rv = commandMap.get(name);
|
||||
if( null!=rv ) return rv;
|
||||
switch(name){
|
||||
case "close": rv = new CloseDbCommand2(); break;
|
||||
case "db": rv = new DbCommand2(); break;
|
||||
case "glob": rv = new GlobCommand2(); break;
|
||||
case "json": rv = new JsonCommand2(); break;
|
||||
case "json-block": rv = new JsonBlockCommand2(); break;
|
||||
case "new": rv = new NewDbCommand2(); break;
|
||||
case "notglob": rv = new NotGlobCommand2(); break;
|
||||
case "null": rv = new NullCommand2(); break;
|
||||
case "oom": rv = new NoopCommand2(); break;
|
||||
case "open": rv = new OpenDbCommand2(); break;
|
||||
case "print": rv = new PrintCommand2(); break;
|
||||
case "result": rv = new ResultCommand2(); break;
|
||||
case "run": rv = new RunCommand2(); break;
|
||||
case "tableresult": rv = new TableResultCommand2(); break;
|
||||
case "testcase": rv = new TestCaseCommand2(); break;
|
||||
default: rv = null; break;
|
||||
}
|
||||
if( null!=rv ) commandMap.put(name, rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**
|
||||
Treats argv[0] as a command name, looks it up with
|
||||
getCommandByName(), and calls process() on that instance, passing
|
||||
it arguments given to this function.
|
||||
*/
|
||||
static void dispatch(SQLTester tester, TestScript2 ts, String[] argv) throws Exception{
|
||||
final Command2 cmd = getCommandByName(argv[0]);
|
||||
if(null == cmd){
|
||||
if( tester.skipUnknownCommands() ){
|
||||
ts.warn("skipping remainder because of unknown command '",argv[0],"'.");
|
||||
throw new SkipTestRemainder2(ts);
|
||||
}
|
||||
Util.toss(IllegalArgumentException.class,
|
||||
ts.getOutputPrefix()+": no command handler found for '"+argv[0]+"'.");
|
||||
}
|
||||
cmd.process(tester, ts, argv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
This class represents a single test script. It handles (or
|
||||
delegates) its the reading-in and parsing, but the details of
|
||||
evaluation are delegated elsewhere.
|
||||
*/
|
||||
class TestScript2 {
|
||||
private String filename = null;
|
||||
private String moduleName = null;
|
||||
private final Cursor cur = new Cursor();
|
||||
private final Outer outer = new Outer();
|
||||
|
||||
private static final class Cursor {
|
||||
private final StringBuilder sb = new StringBuilder();
|
||||
byte[] src = null;
|
||||
int pos = 0;
|
||||
int putbackPos = 0;
|
||||
int putbackLineNo = 0;
|
||||
int lineNo = 0 /* yes, zero */;
|
||||
int peekedPos = 0;
|
||||
int peekedLineNo = 0;
|
||||
boolean inComment = false;
|
||||
|
||||
void reset(){
|
||||
sb.setLength(0); pos = 0; lineNo = 0/*yes, zero*/; inComment = false;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readFile(String filename) throws Exception {
|
||||
return java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filename));
|
||||
}
|
||||
|
||||
/**
|
||||
Initializes the script with the content of the given file.
|
||||
Throws if it cannot read the file.
|
||||
*/
|
||||
public TestScript2(String filename) throws Exception{
|
||||
this.filename = filename;
|
||||
setVerbosity(2);
|
||||
cur.src = readFile(filename);
|
||||
}
|
||||
|
||||
public String getFilename(){
|
||||
return filename;
|
||||
}
|
||||
|
||||
public String getModuleName(){
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
public void setVerbosity(int level){
|
||||
outer.setVerbosity(level);
|
||||
}
|
||||
|
||||
public String getOutputPrefix(){
|
||||
return "["+(moduleName==null ? filename : moduleName)+"] line "+
|
||||
cur.lineNo;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
private TestScript2 verboseN(int level, Object... vals){
|
||||
final int verbosity = outer.getVerbosity();
|
||||
if(verbosity>=level){
|
||||
outer.out("VERBOSE",(verbosity>1 ? "+ " : " "),
|
||||
getOutputPrefix(),": ");
|
||||
outer.outln(vals);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private TestScript2 verbose1(Object... vals){return verboseN(1,vals);}
|
||||
private TestScript2 verbose2(Object... vals){return verboseN(2,vals);}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public TestScript2 warn(Object... vals){
|
||||
outer.out("WARNING ", getOutputPrefix(),": ");
|
||||
outer.outln(vals);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void tossSyntax(Object... msg){
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(this.filename).append(":").append(cur.lineNo).
|
||||
append(": ");
|
||||
for(Object o : msg) sb.append(o);
|
||||
throw new RuntimeException(sb.toString());
|
||||
}
|
||||
|
||||
private void reset(){
|
||||
cur.reset();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Returns the next line from the buffer, minus the trailing EOL.
|
||||
|
||||
Returns null when all input is consumed. Throws if it reads
|
||||
illegally-encoded input, e.g. (non-)characters in the range
|
||||
128-256.
|
||||
*/
|
||||
String getLine(){
|
||||
if( cur.pos==cur.src.length ){
|
||||
return null /* EOF */;
|
||||
}
|
||||
cur.putbackPos = cur.pos;
|
||||
cur.putbackLineNo = cur.lineNo;
|
||||
cur.sb.setLength(0);
|
||||
final boolean skipLeadingWs = false;
|
||||
byte b = 0, prevB = 0;
|
||||
int i = cur.pos;
|
||||
if(skipLeadingWs) {
|
||||
/* Skip any leading spaces, including newlines. This will eliminate
|
||||
blank lines. */
|
||||
for(; i < cur.src.length; ++i, prevB=b){
|
||||
b = cur.src[i];
|
||||
switch((int)b){
|
||||
case 32/*space*/: case 9/*tab*/: case 13/*CR*/: continue;
|
||||
case 10/*NL*/: ++cur.lineNo; continue;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if( i==cur.src.length ){
|
||||
return null /* EOF */;
|
||||
}
|
||||
}
|
||||
boolean doBreak = false;
|
||||
final byte[] aChar = {0,0,0,0} /* multi-byte char buffer */;
|
||||
int nChar = 0 /* number of bytes in the char */;
|
||||
for(; i < cur.src.length && !doBreak; ++i){
|
||||
b = cur.src[i];
|
||||
switch( (int)b ){
|
||||
case 13/*CR*/: continue;
|
||||
case 10/*NL*/:
|
||||
++cur.lineNo;
|
||||
if(cur.sb.length()>0) doBreak = true;
|
||||
// Else it's an empty string
|
||||
break;
|
||||
default:
|
||||
/* Multi-byte chars need to be gathered up and appended at
|
||||
one time. Appending individual bytes to the StringBuffer
|
||||
appends their integer value. */
|
||||
nChar = 1;
|
||||
switch( b & 0xF0 ){
|
||||
case 0xC0: nChar = 2; break;
|
||||
case 0xE0: nChar = 3; break;
|
||||
case 0xF0: nChar = 4; break;
|
||||
default:
|
||||
if( b > 127 ) tossSyntax("Invalid character (#"+(int)b+").");
|
||||
break;
|
||||
}
|
||||
if( 1==nChar ){
|
||||
cur.sb.append((char)b);
|
||||
}else{
|
||||
for(int x = 0; x < nChar; ++x) aChar[x] = cur.src[i+x];
|
||||
cur.sb.append(new String(Arrays.copyOf(aChar, nChar),
|
||||
StandardCharsets.UTF_8));
|
||||
i += nChar-1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
cur.pos = i;
|
||||
final String rv = cur.sb.toString();
|
||||
if( i==cur.src.length && 0==rv.length() ){
|
||||
return null /* EOF */;
|
||||
}
|
||||
return rv;
|
||||
}/*getLine()*/
|
||||
|
||||
/**
|
||||
Fetches the next line then resets the cursor to its pre-call
|
||||
state. consumePeeked() can be used to consume this peeked line
|
||||
without having to re-parse it.
|
||||
*/
|
||||
public String peekLine(){
|
||||
final int oldPos = cur.pos;
|
||||
final int oldPB = cur.putbackPos;
|
||||
final int oldPBL = cur.putbackLineNo;
|
||||
final int oldLine = cur.lineNo;
|
||||
final String rc = getLine();
|
||||
cur.peekedPos = cur.pos;
|
||||
cur.peekedLineNo = cur.lineNo;
|
||||
cur.pos = oldPos;
|
||||
cur.lineNo = oldLine;
|
||||
cur.putbackPos = oldPB;
|
||||
cur.putbackLineNo = oldPBL;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
Only valid after calling peekLine() and before calling getLine().
|
||||
This places the cursor to the position it would have been at had
|
||||
the peekLine() had been fetched with getLine().
|
||||
*/
|
||||
public void consumePeeked(){
|
||||
cur.pos = cur.peekedPos;
|
||||
cur.lineNo = cur.peekedLineNo;
|
||||
}
|
||||
|
||||
/**
|
||||
Restores the cursor to the position it had before the previous
|
||||
call to getLine().
|
||||
*/
|
||||
public void putbackLine(){
|
||||
cur.pos = cur.putbackPos;
|
||||
cur.lineNo = cur.putbackLineNo;
|
||||
}
|
||||
|
||||
private static final Pattern patternRequiredProperties =
|
||||
Pattern.compile(" REQUIRED_PROPERTIES:[ \\t]*(\\S.*)\\s*$");
|
||||
private static final Pattern patternScriptModuleName =
|
||||
Pattern.compile(" SCRIPT_MODULE_NAME:[ \\t]*(\\S+)\\s*$");
|
||||
private static final Pattern patternMixedModuleName =
|
||||
Pattern.compile(" ((MIXED_)?MODULE_NAME):[ \\t]*(\\S+)\\s*$");
|
||||
private static final Pattern patternCommand =
|
||||
Pattern.compile("^--(([a-z-]+)( .*)?)$");
|
||||
|
||||
/**
|
||||
Looks for "directives." If a compatible one is found, it is
|
||||
processed and this function returns. If an incompatible one is found,
|
||||
a description of it is returned and processing of the test must
|
||||
end immediately.
|
||||
*/
|
||||
private void checkForDirective(String line) throws IncompatibleDirective {
|
||||
if(line.startsWith("#")){
|
||||
throw new IncompatibleDirective(this, "C-preprocessor input: "+line);
|
||||
}else if(line.startsWith("---")){
|
||||
new IncompatibleDirective(this, "triple-dash: "+line);
|
||||
}
|
||||
Matcher m = patternScriptModuleName.matcher(line);
|
||||
if( m.find() ){
|
||||
moduleName = m.group(1);
|
||||
return;
|
||||
}
|
||||
m = patternRequiredProperties.matcher(line);
|
||||
if( m.find() ){
|
||||
throw new IncompatibleDirective(this, "REQUIRED_PROPERTIES: "+m.group(1));
|
||||
}
|
||||
m = patternMixedModuleName.matcher(line);
|
||||
if( m.find() ){
|
||||
throw new IncompatibleDirective(this, m.group(1)+": "+m.group(3));
|
||||
}
|
||||
if( line.indexOf("\n|")>=0 ){
|
||||
throw new IncompatibleDirective(this, "newline-pipe combination.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public boolean isCommandLine(String line, boolean checkForImpl){
|
||||
final Matcher m = patternCommand.matcher(line);
|
||||
boolean rc = m.find();
|
||||
if( rc && checkForImpl ){
|
||||
rc = null!=CommandDispatcher2.getCommandByName(m.group(2));
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
If line looks like a command, returns an argv for that command
|
||||
invocation, else returns null.
|
||||
*/
|
||||
public String[] getCommandArgv(String line){
|
||||
final Matcher m = patternCommand.matcher(line);
|
||||
return m.find() ? m.group(1).trim().split("\\s+") : null;
|
||||
}
|
||||
|
||||
/**
|
||||
Fetches lines until the next recognized command. Throws if
|
||||
checkForDirective() does. Returns null if there is no input or
|
||||
it's only whitespace. The returned string retains all whitespace.
|
||||
|
||||
Note that "subcommands", --command-like constructs in the body
|
||||
which do not match a known command name are considered to be
|
||||
content, not commands.
|
||||
*/
|
||||
public String fetchCommandBody(){
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while( (null != (line = peekLine())) ){
|
||||
checkForDirective(line);
|
||||
if( !isCommandLine(line, true) ){
|
||||
sb.append(line).append("\n");
|
||||
consumePeeked();
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
line = sb.toString();
|
||||
return line.trim().isEmpty() ? null : line;
|
||||
}
|
||||
|
||||
public void processCommand(SQLTester t, String[] argv) throws Exception{
|
||||
verbose1("running command: ",argv[0], " ", Util.argvToString(argv));
|
||||
if(outer.getVerbosity()>1){
|
||||
final String input = t.getInputText();
|
||||
if( !input.isEmpty() ) verbose2("Input buffer = ",input);
|
||||
}
|
||||
CommandDispatcher2.dispatch(t, this, argv);
|
||||
}
|
||||
|
||||
public void toss(Object... msg) throws TestScript2Failed {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(Object s : msg) sb.append(s);
|
||||
throw new TestScript2Failed(this, sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
Runs this test script in the context of the given tester object.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean run(SQLTester tester) throws Exception {
|
||||
reset();
|
||||
setVerbosity(tester.getVerbosity());
|
||||
String line, directive;
|
||||
String[] argv;
|
||||
while( null != (line = getLine()) ){
|
||||
//verbose(line);
|
||||
checkForDirective(line);
|
||||
argv = getCommandArgv(line);
|
||||
if( null!=argv ){
|
||||
processCommand(tester, argv);
|
||||
continue;
|
||||
}
|
||||
tester.appendInput(line,true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
** REQUIRED_PROPERTIES:
|
||||
**
|
||||
*/
|
||||
--print starting up 😃
|
||||
--close all
|
||||
--oom
|
||||
--db 0
|
||||
@ -38,5 +39,13 @@ SELECT json_array(1,2,3)
|
||||
[1,2,3]
|
||||
{"a":1,"b":2}
|
||||
--end
|
||||
--testcase col-names-on
|
||||
--column-names 1
|
||||
select 1 as 'a', 2 as 'b';
|
||||
--result a 1 b 2
|
||||
--testcase col-names-off
|
||||
--column-names 0
|
||||
select 1 as 'a', 2 as 'b';
|
||||
--result 1 2
|
||||
--close
|
||||
--print 🤩😃 the end
|
||||
--print reached the end 😃
|
@ -1,68 +0,0 @@
|
||||
/* A script for testing the org.sqlite.jni.tester infrastructure
|
||||
**
|
||||
** SCRIPT_MODULE_NAME: 000_first
|
||||
**
|
||||
*/
|
||||
|
||||
junk
|
||||
|
||||
--new SQLTester.db
|
||||
--run
|
||||
SELECT 1;
|
||||
SELECT 2;
|
||||
-- comment
|
||||
-- uncomment to introduce intentional syntax error
|
||||
--oom
|
||||
--print These are args to the print command.
|
||||
This is from the print command's body.
|
||||
--print
|
||||
Also from the print command.
|
||||
--- also ignored
|
||||
--testcase 1
|
||||
SELECT 'a b', 'c';
|
||||
SELECT 'd', 'e';
|
||||
SELECT '{}', 'f';
|
||||
SELECT '{ }', 'g';
|
||||
SELECT '(a-b-c)', '[a-b-c]';
|
||||
-- this comment must not cause an error
|
||||
--result {a b} c d e "{}" f "{\011}" g (a-b-c) [a-b-c]
|
||||
--testcase 2
|
||||
SELECT 123
|
||||
--glob 1#
|
||||
--testcase 3
|
||||
SELECT 'a'
|
||||
--notglob #
|
||||
--close
|
||||
--open SQLTester.db
|
||||
--print Re-opened db.
|
||||
--testcase fourth
|
||||
SELECT 1, 2;
|
||||
SELECT 'b', 'c';
|
||||
--tableresult
|
||||
[0-9] #
|
||||
b c
|
||||
--end
|
||||
--null zilch
|
||||
--testcase null-command
|
||||
SELECT null;
|
||||
--result zilch
|
||||
--testcase json-array-1
|
||||
SELECT json_array(1,2,3)
|
||||
--json [1,2,3]
|
||||
--testcase json-array-2
|
||||
SELECT json_array(1,2,3);
|
||||
SELECT json_object('a',1,'b',2);
|
||||
--json-block
|
||||
[1,2,3]
|
||||
{"a":1,"b":2}
|
||||
--end
|
||||
--testcase table-result-globs
|
||||
SELECT 123;
|
||||
SELECT 'aBc';
|
||||
SELECT 456;
|
||||
--tableresult
|
||||
#
|
||||
[a-z][A-Z][a-z]
|
||||
4#
|
||||
--end
|
||||
--an-unknown-command
|
Reference in New Issue
Block a user