From: stephan Date: Wed, 9 Aug 2023 22:18:22 +0000 (+0000) Subject: More for the SQLTester rework. Can read input and dispatch commands, but only --print... X-Git-Tag: version-3.43.0~47^2~36 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=bef0369ff463ff634c60f8d75be387e573be51ef;p=thirdparty%2Fsqlite.git More for the SQLTester rework. Can read input and dispatch commands, but only --print is currently implemented. FossilOrigin-Name: 4fa2ad33edbcef393dd98dbf90586ad8f32ec0beab02f197c8038a44be86c314 --- diff --git a/ext/jni/src/org/sqlite/jni/tester/SQLTester.java b/ext/jni/src/org/sqlite/jni/tester/SQLTester.java index e93fd10a70..39047fa98e 100644 --- a/ext/jni/src/org/sqlite/jni/tester/SQLTester.java +++ b/ext/jni/src/org/sqlite/jni/tester/SQLTester.java @@ -80,7 +80,6 @@ public class SQLTester { private int iCurrentDb = 0; private final String initialDbName = "test.db"; private TestScript currentScript; - private TestScript2 currentScript2; public SQLTester(){ reset(); @@ -159,23 +158,23 @@ public class SQLTester { //! Not yet funcional private void runTests2() throws Exception { - try { - for(String f : listInFiles){ - reset(); - setupInitialDb(); - ++nTestFile; - final TestScript2 ts = new TestScript2(f); - currentScript2 = ts; - try{ - ts.run(this); - }catch(SkipTestRemainder e){ - /* not an error */ - ++nAbortedScript; - } - outln("<<<<<----- ",nTest," test(s) in ",ts.getFilename()); + for(String f : listInFiles){ + reset(); + setupInitialDb(); + ++nTestFile; + final TestScript2 ts = new TestScript2(f); + try{ + ts.run(this); + }catch(SkipTestRemainder2 e){ + /* not fatal */ + outln(e); + ++nAbortedScript; + }catch(IncompatibleDirective e){ + /* not fatal */ + outln(e); + ++nAbortedScript; } - }finally{ - currentScript2 = null; + outln("<<<<<----- ",nTest," test(s) in ",ts.getFilename()); } Util.unlink(initialDbName); } @@ -195,6 +194,16 @@ public class SQLTester { StringBuilder getInputBuffer(){ return inputBuffer; } + void appendInput(String n, boolean addNL){ + inputBuffer.append(n); + if(addNL) inputBuffer.append('\n'); + } + + void appendResult(String n, boolean addNL){ + resultBuffer.append(n); + if(addNL) resultBuffer.append('\n'); + } + String getInputText(){ return inputBuffer.toString(); } String getResultText(){ return resultBuffer.toString(); } diff --git a/ext/jni/src/org/sqlite/jni/tester/TestScript.java b/ext/jni/src/org/sqlite/jni/tester/TestScript.java index d1cdf1e238..742cf4fa52 100644 --- a/ext/jni/src/org/sqlite/jni/tester/TestScript.java +++ b/ext/jni/src/org/sqlite/jni/tester/TestScript.java @@ -90,7 +90,7 @@ class TestScript { } @SuppressWarnings("unchecked") - private TestScript verbose(T... vals){ + private TestScript verbose(Object... vals){ outer.verbose(vals); return this; } diff --git a/ext/jni/src/org/sqlite/jni/tester/TestScript2.java b/ext/jni/src/org/sqlite/jni/tester/TestScript2.java index 4c9c5bdb3b..013c46cd12 100644 --- a/ext/jni/src/org/sqlite/jni/tester/TestScript2.java +++ b/ext/jni/src/org/sqlite/jni/tester/TestScript2.java @@ -12,9 +12,123 @@ ** This file contains the TestScript2 part of the SQLTester framework. */ package org.sqlite.jni.tester; -//import java.util.regex.*; import java.util.Arrays; import java.nio.charset.StandardCharsets; +import java.util.regex.*; + +class SQLTestException extends RuntimeException { + public SQLTestException(String msg){ + super(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=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); + } +} + +class PrintCommand2 extends Command2 { + public void process( + SQLTester st, TestScript2 ts, String[] argv + ) throws Exception{ + st.out(ts.getOutputPrefix(),": "); + if( 1==argv.length ){ + st.outln( st.getInputText() ); + }else{ + st.outln( Util.argvToString(argv) ); + } + final String body = ts.fetchCommandBody(); + if( null!=body ){ + st.out(body,"\n"); + } + } +} + +class CommandDispatcher2 { + + private static java.util.Map 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 "print": rv = new PrintCommand2(); 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 @@ -23,18 +137,23 @@ import java.nio.charset.StandardCharsets; */ class TestScript2 { private String filename = null; - private final Cursor curs = new Cursor(); + 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 lineNo = 1; + 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 = 1; inComment = false; + sb.setLength(0); pos = 0; lineNo = 0/*yes, zero*/; inComment = false; } } @@ -49,76 +168,102 @@ class TestScript2 { public TestScript2(String filename) throws Exception{ this.filename = filename; setVerbosity(2); - curs.src = readFile(filename); + 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 verbose(T... vals){ - outer.verbose(vals); + private TestScript2 verbose(Object... vals){ + final int verbosity = outer.getVerbosity(); + if(verbosity>0){ + outer.out("VERBOSE",(verbosity>1 ? "+ " : " "), + getOutputPrefix(),": "); + outer.outln(vals); + } + return this; + } + + @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(curs.lineNo). + sb.append(this.filename).append(":").append(cur.lineNo). append(": "); for(Object o : msg) sb.append(o); throw new RuntimeException(sb.toString()); } private void reset(){ - curs.reset(); + cur.reset(); } + /** Returns the next line from the buffer, minus the trailing EOL. - If skipLeadingWs is true then all leading whitespace (including - blank links) is skipped over and will not appear in the resulting - string. 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(boolean skipLeadingWs){ - curs.sb.setLength(0); + 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 = curs.pos; + int i = cur.pos; if(skipLeadingWs) { /* Skip any leading spaces, including newlines. This will eliminate blank lines. */ - for(; i < curs.src.length; ++i, prevB=b){ - b = curs.src[i]; + 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*/: ++curs.lineNo; continue; + case 10/*NL*/: ++cur.lineNo; continue; default: break; } break; } - } - if( i==curs.src.length ){ - return null /* EOF */; + 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 < curs.src.length && !doBreak; ++i){ - b = curs.src[i]; + for(; i < cur.src.length && !doBreak; ++i){ + b = cur.src[i]; switch( (int)b ){ case 13/*CR*/: continue; case 10/*NL*/: - ++curs.lineNo; - if(curs.sb.length()>0) doBreak = true; + ++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 @@ -134,33 +279,161 @@ class TestScript2 { break; } if( 1==nChar ){ - curs.sb.append((char)b); + cur.sb.append((char)b); }else{ - for(int x = 0; x < nChar; ++x) aChar[x] = curs.src[i+x]; - curs.sb.append(new String(Arrays.copyOf(aChar, nChar), + 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; } } - curs.pos = i; - if( 0==curs.sb.length() && i==curs.src.length ){ + cur.pos = i; + final String rv = cur.sb.toString(); + if( i==cur.src.length && 0==rv.length() ){ return null /* EOF */; } - return curs.sb.toString(); + 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*$"); + 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)); + } + return; + } + + public boolean isCommandLine(String line){ + final Matcher m = patternCommand.matcher(line); + return m.find(); + } + + /** + 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 command. Throws if + checkForDirective() does. Returns null if there is no input or + it's only whitespace. The returned string is trim()'d of + leading/trailing whitespace. + */ + public String fetchCommandBody(){ + final StringBuilder sb = new StringBuilder(); + String line; + while( (null != (line = peekLine())) ){ + checkForDirective(line); + if( !isCommandLine(line) ){ + sb.append(line).append("\n"); + consumePeeked(); + }else{ + break; + } + } + line = sb.toString().trim(); + return line.isEmpty() ? null : line; + } + + public void processCommand(SQLTester t, String[] argv) throws Exception{ + //verbose("got argv: ",argv[0], " ", Util.argvToString(argv)); + //verbose("Input buffer = ",t.getInputBuffer()); + CommandDispatcher2.dispatch(t, this, argv); + } + /** Runs this test script in the context of the given tester object. */ @SuppressWarnings("unchecked") - public void run(SQLTester tester) throws Exception { + public boolean run(SQLTester tester) throws Exception { reset(); setVerbosity(tester.getVerbosity()); - String line; - while( null != (line = getLine(false)) ){ - verbose("LINE #",curs.lineNo-1,": ",line); + String line, directive; + String[] argv; + while( null != (line = getLine()) ){ + //verbose(line); + checkForDirective(line); + argv = getCommandArgv(line); + if( null!=argv ){ + processCommand(tester, argv); + continue; + } + tester.appendInput(line,true); } + return true; } } diff --git a/ext/jni/src/tests/000-000-sanity.test2 b/ext/jni/src/tests/000-000-sanity.test2 index 24e1b68a14..241305c0f8 100644 --- a/ext/jni/src/tests/000-000-sanity.test2 +++ b/ext/jni/src/tests/000-000-sanity.test2 @@ -1,10 +1,20 @@ /* ** This is a comment. There are many like it but this one is mine. ** - */ ---command 1 +** SCRIPT_MODULE_NAME: sanity-check +** xMIXED_MODULE_NAME: mixed-module +** xMODULE_NAME: module-name +** xREQUIRED_PROPERTIES: small fast reliable +** +*/ +/*#if foo*/ +/*---foo*/ +/* --print without flags dumps current input buffer */ +--print ---command 🤩😃 +non-command/non-directive text after --print is also emitted. +--print 🤩😃 SELECT 1; SELECT 2; +--print the end diff --git a/manifest b/manifest index e48221f32f..93a495d6f2 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Initial\ssketches\sof\sa\sline-by-line\sparser\sfor\sSQLTester\sto\sovercome\sits\scompatibility\sshortcomings.\sFar\sfrom\scomplete. -D 2023-08-09T19:51:39.077 +C More\sfor\sthe\sSQLTester\srework.\sCan\sread\sinput\sand\sdispatch\scommands,\sbut\sonly\s--print\sis\scurrently\simplemented. +D 2023-08-09T22:18:22.852 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -266,11 +266,11 @@ F ext/jni/src/org/sqlite/jni/sqlite3_context.java d26573fc7b309228cb49786e907859 F ext/jni/src/org/sqlite/jni/sqlite3_stmt.java 78e6d1b95ac600a9475e9db4623f69449322b0c93d1bd4e1616e76ed547ed9fc F ext/jni/src/org/sqlite/jni/sqlite3_value.java 3d1d4903e267bc0bc81d57d21f5e85978eff389a1a6ed46726dbe75f85e6914a F ext/jni/src/org/sqlite/jni/tester/Outer.java b06acf9c79e8dbc8fea4a98b00724a6a76e3ee4503eb114671d2885f8fb3df8b -F ext/jni/src/org/sqlite/jni/tester/SQLTester.java 35ea65f416b41b1e21119f58109e35518a39cbc1edbdd8883cb581772665c18d -F ext/jni/src/org/sqlite/jni/tester/TestScript.java 18f55e1e3001c4ccfc359d57448729227c3eaf4a7c774964fe6418e07aefd541 -F ext/jni/src/org/sqlite/jni/tester/TestScript2.java 1c8426039f2050cf623dab17e5d05a976ee061429c7be1eb5a3e73f7b00daf65 +F ext/jni/src/org/sqlite/jni/tester/SQLTester.java 2663dffe3977b73730ba3cbdd6dc0fe053699479759b75bb46c1f966773f0b76 +F ext/jni/src/org/sqlite/jni/tester/TestScript.java 463021981a65ffe7147a1bfada557b275b0cba3c33176ac328502ff09d146f28 +F ext/jni/src/org/sqlite/jni/tester/TestScript2.java f01faf9facbc029f5baf731edf5de37cdd2921ec55ca4d8bd1e764c13211828f F ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md ab7169b08566a082ef55c9ef8a553827f99958ed3e076f31eef757563fae51ba -F ext/jni/src/tests/000-000-sanity.test2 0b4bb90e12cfa96e24999f5890ce46305b6a83efcf9ef0ff4df16521f636a9d5 +F ext/jni/src/tests/000-000-sanity.test2 8cb312a3dde4d09ead6a33a056ef685a99225b18635a07d03fa21d83879fb1c0 F ext/jni/src/tests/000_first.test cd5fb732520cf36d7a3e5ad94a274c7327a9504b01a1a7f98e1f946df6c539fd F ext/jni/src/tests/010_ignored.test e17e874c6ab3c437f1293d88093cf06286083b65bf162317f91bbfd92f961b70 F ext/lsm1/Makefile a553b728bba6c11201b795188c5708915cc4290f02b7df6ba7e8c4c943fd5cd9 @@ -2092,8 +2092,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P f937097e9b22a6c78c242cbf00c71bdc57f04b1b9a15ae24058bc2813c99688c -R e3dd73f0afa45739c2d0daea22589f05 +P 43534cd042499c1bef44ca5c4a8305a710d99e70e8b0adce6df50c6a1f0402b9 +R 02b26ab163237a92c9aeaab8dbe6493d U stephan -Z 24fe8a8f37b2f666f1b6b1c10180bf33 +Z 4fb10f0d619801f5f787d45bc947ea2c # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index eaf5ea6314..c6f2dd21f8 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -43534cd042499c1bef44ca5c4a8305a710d99e70e8b0adce6df50c6a1f0402b9 \ No newline at end of file +4fa2ad33edbcef393dd98dbf90586ad8f32ec0beab02f197c8038a44be86c314 \ No newline at end of file