🌐 AI搜索 & 代理 主页
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