Initial code for a test program specifically for QRF. Maybe it would be
better just to use the TCL interface for this. Worked saved on this branch
in case I ever decide to come back to it or reuse part of it.
FossilOrigin-Name: ad6a56a2d2e23fb5abd9f42f8bf54044b20a987b62a91e6a9d6c70f374bcb15d
diff --git a/ext/qrf/test/qrftest.c b/ext/qrf/test/qrftest.c
new file mode 100644
index 0000000..8905900
--- /dev/null
+++ b/ext/qrf/test/qrftest.c
@@ -0,0 +1,360 @@
+/*
+** 2025-12-01
+**
+** 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.
+**
+*************************************************************************
+**
+** A test harness for QRF.
+*/
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <ctype.h>
+#include "sqlite3.h"
+#include "qrf.h"
+
+/* Integer types */
+typedef sqlite3_int64 i64;
+typedef unsigned char u8;
+
+/*
+** State object for the test
+*/
+typedef struct QrfTest QrfTest;
+struct QrfTest {
+ int nErr; /* Number of errors */
+ int nTest; /* Number of test cases */
+ sqlite3 *db; /* Database connection used for tests */
+ const char *zFilename; /* Input filename */
+ i64 nLine; /* Line number of last line of input read */
+ FILE *in; /* Input file stream */
+ sqlite3_str *pExpected; /* Expected results */
+ char *zResult; /* Results written here */
+ sqlite3_str *pResult; /* Or here */
+ sqlite3_str *pSql; /* Accumulated SQL script */
+ sqlite3_qrf_spec spec; /* Output format spec */
+};
+
+/*
+** Report OOM and die
+*/
+static void qrfTestOom(void){
+ printf("Out of memory\n");
+ exit(1);
+}
+
+/*
+** Change a string value in spec.
+*/
+static void qrfTestSetStr(char **pz, const char *z){
+ size_t n = strlen(z);
+ if( (*pz)!=0 ) sqlite3_free(*pz);
+ if( z==0 || strcmp(z,"<NULL>")==0 ){
+ *pz = 0;
+ }else{
+ *pz = sqlite3_malloc64( n+1 );
+ if( (*pz)==0 ) qrfTestOom();
+ memcpy(*pz, z, n);
+ (*pz)[n] = 0;
+ }
+}
+
+/*
+** Free all resources held by p->spec.
+*/
+static void qrfTestResetSpec(QrfTest *p){
+ sqlite3_free(p->spec.aWidth);
+ sqlite3_free(p->spec.aAlign);
+ sqlite3_free(p->spec.zColumnSep);
+ sqlite3_free(p->spec.zRowSep);
+ sqlite3_free(p->spec.zTableName);
+ sqlite3_free(p->spec.zNull);
+ memset(&p->spec, 0, sizeof(p->spec));
+ p->spec.iVersion = 1;
+}
+
+/*
+** Free all memory resources held by p.
+*/
+static void qrfTestReset(QrfTest *p){
+ if( p->in ){ fclose(p->in); p->in = 0; }
+ if( p->db ){ sqlite3_close(p->db); p->db = 0; }
+ if( p->pExpected ){ sqlite3_str_free(p->pExpected); p->pExpected = 0; }
+ if( p->zResult ){ sqlite3_free(p->zResult); p->zResult = 0; }
+ if( p->pResult ){ sqlite3_str_free(p->pResult); p->pResult = 0; }
+ qrfTestResetSpec(p);
+}
+
+/*
+** Report an error
+*/
+static void qrfTestError(QrfTest *p, const char *zFormat, ...){
+ va_list ap;
+ sqlite3_str *pErr;
+ va_start(ap, zFormat);
+ pErr = sqlite3_str_new(p->db);
+ sqlite3_str_vappendf(pErr, zFormat, ap);
+ va_end(ap);
+ printf("%s:%d: %s\n", p->zFilename, (int)p->nLine, sqlite3_str_value(pErr));
+ sqlite3_str_free(pErr);
+ p->nErr++;
+}
+
+/*
+** Return a pointer to the next token in the input, or NULL if there
+** are no more tokens. Leave *pz pointing to the first character past
+** the end of the token.
+*/
+static const char *nextToken(char **pz, int eMode){
+ char *z = *pz;
+ char *zReturn = 0;
+ while( isspace(z[0]) ) z++;
+ if( eMode!=1 && z[0]=='*' && z[1]=='/' ) return 0;
+ zReturn = z;
+ if( z[0]==0 ) return 0;
+ while( z[0] && !isspace(z[0]) ){ z++; }
+ if( z[0] ){
+ z[0] = 0;
+ z++;
+ }
+ *pz = z;
+ return zReturn;
+}
+
+/* Arrays of names that map symbol names into numeric constants. */
+
+static const char *azStyle[] = {
+ "auto", "box", "column", "count", "csv", "eqp", "explain",
+ "html", "insert", "json", "jobject", "line", "list", "markdown",
+ "off", "quote", "stats", "statsest", "statsvm", "table", 0
+};
+static const char *azEsc[] = {
+ "auto", "off", "ascii", "symbol", 0
+};
+static const char *azText[] = {
+ "auto", "plain", "sql", "csv", "html", "tcl", "json", 0
+};
+static const char *azBlob[] = {
+ "auto", "text", "sql", "hex", "tcl", "json", "size", 0
+};
+static const char *azBool[] = {
+ "auto", "off", "on", 0
+};
+static const char *azAlign[] = { "auto", "left", "right", "center", 0 };
+
+/*
+** Find the match for zArg, and azChoice[] and return its index.
+** If not found, issue an error message and return 0;
+*/
+static int findChoice(
+ QrfTest *p,
+ const char *zKey,
+ const char *zArg,
+ const char *const*azChoice
+){
+ int i;
+ sqlite3_str *pErr;
+ if( zArg==0 ){
+ qrfTestError(p, "missing argument to \"%s\"", zKey);
+ return 0;
+ }
+ for(i=0; azChoice[i]; i++){
+ if( strcmp(zArg,azChoice[i])==0 ) return i;
+ }
+ pErr = sqlite3_str_new(p->db);
+ sqlite3_str_appendf(pErr, "argument to %s should be one of:");
+ for(i=0; azChoice[i]; i++){
+ sqlite3_str_appendf(pErr, " %s", azChoice[i]);
+ }
+ qrfTestError(p, "%z", sqlite3_str_finish(pErr));
+ return 0;
+}
+
+/*
+** zLine[] contains text that changes values of p->spec. Parse that
+** line and make appropriate changes.
+**
+** Return 0 if zLine[] ends with and end-of-comment. Return 1 if the
+** spec definition is to continue.
+*/
+static int qrfTestParseSpec(QrfTest *p, char *zLine){
+ const char *zToken;
+ while( (zToken = nextToken(&zLine,1))!=0 ){
+ if( strcmp(zToken,"*/")==0 ) return 0;
+ if( strcmp(zToken,"eStyle")==0 ){
+ p->spec.eStyle = findChoice(p, zToken, nextToken(&zLine,0), azStyle);
+ }else
+ if( strcmp(zToken,"eEsc")==0 ){
+ p->spec.eEsc = findChoice(p, zToken, nextToken(&zLine,0), azEsc);
+ }else
+ if( strcmp(zToken,"eText")==0 ){
+ p->spec.eText = findChoice(p, zToken, nextToken(&zLine,0), azText);
+ }else
+ if( strcmp(zToken,"eTitle")==0 ){
+ p->spec.eTitle = findChoice(p, zToken, nextToken(&zLine,0), azText);
+ }else
+ if( strcmp(zToken,"eBlob")==0 ){
+ p->spec.eBlob = findChoice(p, zToken, nextToken(&zLine,0), azBlob);
+ }else
+ if( strcmp(zToken,"bTitles")==0 ){
+ p->spec.bTitles = findChoice(p, zToken, nextToken(&zLine,0),azBool);
+ }else
+ if( strcmp(zToken,"bWordWrap")==0 ){
+ p->spec.bWordWrap = findChoice(p, zToken, nextToken(&zLine,0),azBool);
+ }else
+ if( strcmp(zToken,"bTextJsonb")==0 ){
+ p->spec.bTextJsonb = findChoice(p, zToken, nextToken(&zLine,0),azBool);
+ }else
+ if( strcmp(zToken,"eDfltAlign")==0 ){
+ p->spec.eDfltAlign = findChoice(p, zToken, nextToken(&zLine,0),azAlign);
+ }else
+ if( strcmp(zToken,"eTitleAlign")==0 ){
+ p->spec.eTitleAlign = findChoice(p, zToken, nextToken(&zLine,0),azAlign);
+ }else
+ if( strcmp(zToken,"bSplitColumn")==0 ){
+ p->spec.bSplitColumn = findChoice(p, zToken, nextToken(&zLine,0),azBool);
+ }else
+ if( strcmp(zToken,"bBorder")==0 ){
+ p->spec.bBorder = findChoice(p, zToken, nextToken(&zLine,0),azBool);
+ }else
+ if( strcmp(zToken,"zColumnSep")==0 ){
+ qrfTestSetStr(&p->spec.zColumnSep, nextToken(&zLine,0));
+ }else
+ if( strcmp(zToken,"zRowSep")==0 ){
+ qrfTestSetStr(&p->spec.zRowSep, nextToken(&zLine,0));
+ }else
+ if( strcmp(zToken,"zTableName")==0 ){
+ qrfTestSetStr(&p->spec.zTableName, nextToken(&zLine,0));
+ }else
+ if( strcmp(zToken,"zNull")==0 ){
+ qrfTestSetStr(&p->spec.zNull, nextToken(&zLine,0));
+ }else
+ {
+ qrfTestError(p, "unknown spec key: \"%s\"", zToken);
+ }
+ }
+ return 1;
+}
+
+/*
+** Read and run a single test script.
+**
+** The file is SQL text. Special C-style comments control the testing.
+** Because this description is itself within a C-style comment, the comment
+** delimiters are shown as (*...*), with parentheses instead of "/".
+**
+** (* spec KEYWORD VALUE ... *)
+**
+** Fill out the p->spec field to use for the next test.
+**
+** (* result
+** ** EXPECTED
+** *)
+**
+** Run QRF and compare results against EXPECTED, with leading "** "
+** removed.
+**
+*/
+static void qrfTestOneFile(QrfTest *p, const char *zFilename){
+ int rc; /* Result code */
+ int eMode; /* 0 = gather SQL. 1 = spec. 2 = gather result */
+ char zLine[4000]; /* One line of input */
+
+ p->nLine = 0;
+ p->zFilename = zFilename;
+ p->in = 0;
+ p->zResult = 0;
+ p->pResult = 0;
+ p->pExpected = 0;
+ p->pSql = 0;
+ memset(&p->spec, 0, sizeof(p->spec));
+ p->spec.iVersion = 1;
+ rc = sqlite3_open(":memory:", &p->db);
+ if( rc ){
+ qrfTestError(p, "cannot open an in-memory database");
+ return;
+ }
+ p->in = fopen(zFilename, "rb");
+ if( p->in==0 ){
+ qrfTestError(p, "cannot open input file \"%s\"", zFilename);
+ qrfTestReset(p);
+ return;
+ }
+ p->pSql = sqlite3_str_new(p->db);
+ p->pExpected = sqlite3_str_new(p->db);
+ while( fgets(zLine, sizeof(zLine), p->in) ){
+ size_t n = strlen(zLine);
+ int done = 0;
+ p->nLine++;
+ if( n==0 ) continue;
+ if( zLine[n-1]!='\n' ){
+ qrfTestError(p, "input line too long. Max length %d",(int)sizeof(zLine));
+ qrfTestReset(p);
+ return;
+ }
+ do{
+ done = 1;
+ switch( eMode ){
+ case 0: { /* Gathering SQL text */
+ if( strncmp(zLine, "/* spec", 7)==0 ){
+ memmove(zLine, &zLine[7], n-7);
+ n-= 7;
+ eMode = 1;
+ done = 0;
+ }else if( strncmp(zLine, "/* result", 9)==0 ){
+ /* qrfTestRunSql(p); */
+ sqlite3_str_truncate(p->pExpected, 0);
+ eMode = 2;
+ }else{
+ sqlite3_str_append(p->pSql, zLine, n);
+ }
+ break;
+ }
+ case 1: { /* Processing spec descriptions */
+ eMode = qrfTestParseSpec(p, zLine);
+ break;
+ }
+ case 2: {
+ if( strncmp(zLine, "*/",2)==0 ){
+ }else if( strcmp(zLine, "**\n")==0 ){
+ sqlite3_str_append(p->pExpected, "\n", 1);
+ }else if( strncmp(zLine, "** ",3)==0 ){
+ sqlite3_str_append(p->pExpected, zLine+3, n-3);
+ }else{
+ qrfTestError(p, "bad result line");
+ }
+ break;
+ }
+ } /* End of switch(eMode) */
+ }while( !done );
+ }
+ qrfTestReset(p);
+ if( sqlite3_memory_used()>0 ){
+ qrfTestError(p, "Memory leak: %lld bytes", sqlite3_memory_used());
+ }
+ p->nTest++;
+}
+
+/*
+** Program entry point
+*/
+int main(int argc, char **argv){
+ int i;
+ QrfTest x;
+ memset(&x, 0, sizeof(x));
+ for(i=1; i<argc; i++){
+ int nErr = x.nErr;
+ qrfTestOneFile(&x, argv[i]);
+ if( x.nErr>nErr ){
+ printf("%s: %d error%s\n", argv[i], x.nErr-nErr, (x.nErr>nErr+1)?"s":"");
+ }
+ }
+ printf("Test cases: %d Errors: %d\n", x.nTest, x.nErr);
+}
diff --git a/main.mk b/main.mk
index 627b702..07d33ee 100644
--- a/main.mk
+++ b/main.mk
@@ -1799,6 +1799,9 @@
$$TCL_LIB_SPEC $$TCL_INCLUDE_SPEC \
$(LDFLAGS.libsqlite3)
+qrftest$(T.exe): sqlite3.c $(TOP)/ext/qrf/qrf.h $(TOP)/ext/qrf/qrf.c $(TOP)/ext/qrf/test/qrftest.c
+ $(T.link) -I$(TOP)/ext/qrf $(TOP)/ext/qrf/test/qrftest.c $(TOP)/ext/qrf/qrf.c sqlite3.c -o $@ $(LDFLAGS.libsqlite3)
+
coretestprogs: testfixture$(B.exe) sqlite3$(B.exe)
testprogs: $(TESTPROGS) srcck1$(B.exe) fuzzcheck$(T.exe) sessionfuzz$(T.exe)
diff --git a/manifest b/manifest
index 78ce359..2465654 100644
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Disallow\sthe\sundocumented\s-recovery-db\soption\son\sthe\s".recover"\scommand\nof\sthe\sCLI\swhen\sin\s--safe\smode.
-D 2025-12-01T11:07:31.854
+C Initial\scode\sfor\sa\stest\sprogram\sspecifically\sfor\sQRF.\s\sMaybe\sit\swould\sbe\nbetter\sjust\sto\suse\sthe\sTCL\sinterface\sfor\sthis.\s\sWorked\ssaved\son\sthis\sbranch\nin\scase\sI\sever\sdecide\sto\scome\sback\sto\sit\sor\sreuse\spart\sof\sit.
+D 2025-12-02T11:40:45.005
F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -419,6 +419,7 @@
F ext/qrf/README.md 86fc5c3c5e3eddbe54fc1235cbdc52b8c2c0732791d224345c3014cd45c4c0e7
F ext/qrf/qrf.c 425d02cffcc5b5fe9ff5817794bf751b1fdd6912f570c354272429ce1262b866
F ext/qrf/qrf.h 322d48537a5aa39c206c2ec0764a7938ea7662a8c25be1c4e9d742789609ba1e
+F ext/qrf/test/qrftest.c a2061dc04604dded25305b32689a01c567a75d8b015d3100116f16ee5a643c8c
F ext/rbu/rbu.c 801450b24eaf14440d8fd20385aacc751d5c9d6123398df41b1b5aa804bf4ce8
F ext/rbu/rbu1.test 25870dd7db7eb5597e2b4d6e29e7a7e095abf332660f67d89959552ce8f8f255
F ext/rbu/rbu10.test 7c22caa32c2ff26983ca8320779a31495a6555737684af7aba3daaf762ef3363
@@ -657,7 +658,7 @@
F ext/wasm/tests/opfs/sahpool/sahpool-pausing.js f264925cfc82155de38cecb3d204c36e0f6991460fff0cb7c15079454679a4e2
F ext/wasm/tests/opfs/sahpool/sahpool-worker.js bd25a43fc2ab2d1bafd8f2854ad3943ef673f7c3be03e95ecf1612ff6e8e2a61
F magic.txt 5ade0bc977aa135e79e3faaea894d5671b26107cc91e70783aa7dc83f22f3ba0
-F main.mk 822f9eda3e689748341597f4327a071c2b0ce41cc3ed477d72f2560b956eb5c0
+F main.mk 5a4954af031ac21dece30ebaf61c9594b8a441c2a0e4bb3636da2687d02ca70c
F make.bat a136fd0b1c93e89854a86d5f4edcf0386d211e5d5ec2434480f6eea436c7420c
F mptest/config01.test 3c6adcbc50b991866855f1977ff172eb6d901271
F mptest/config02.test 4415dfe36c48785f751e16e32c20b077c28ae504
@@ -2180,8 +2181,11 @@
F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7
F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98
F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
-P 35f39f7cb1030b1a57f2921f50ab600496fc1e774593717845e87f2e47dc49ba
-R a7ecee17b65f2e02eed8810e0fb1d4ba
+P 65202440874a7fea5aba262e8e78b97c833147d47837a99f301eca968f9a78b1
+R bd2d2c6a3b8d7b55e3f27798b187abb2
+T *branch * qrftest
+T *sym-qrftest *
+T -sym-trunk *
U drh
-Z 50d000086ea0500f9c141c467282e85a
+Z 2febcbab07f4e3049a7b8ff917242afb
# Remove this line to create a well-formed Fossil manifest.
diff --git a/manifest.tags b/manifest.tags
index bec9717..6a2a44f 100644
--- a/manifest.tags
+++ b/manifest.tags
@@ -1,2 +1,2 @@
-branch trunk
-tag trunk
+branch qrftest
+tag qrftest
diff --git a/manifest.uuid b/manifest.uuid
index 4eb8918..a5a0aca 100644
--- a/manifest.uuid
+++ b/manifest.uuid
@@ -1 +1 @@
-65202440874a7fea5aba262e8e78b97c833147d47837a99f301eca968f9a78b1
+ad6a56a2d2e23fb5abd9f42f8bf54044b20a987b62a91e6a9d6c70f374bcb15d