From 4f3ab29aabdfd127c7ecca4bc080ba82098aec9e Mon Sep 17 00:00:00 2001 From: drh <> Date: Fri, 12 Dec 2025 18:22:07 +0000 Subject: [PATCH] Get the .testcase and .check commands working in the CLI. FossilOrigin-Name: 1ca31e1a297c0d53d068afb658ce6602887cda0f3eaf42cd629c4d1b7204f0b0 --- manifest | 14 +-- manifest.uuid | 2 +- src/shell.c.in | 298 ++++++++++++++++++++++++++++----------------- test/modeA.clitest | 96 +++++++-------- 4 files changed, 242 insertions(+), 168 deletions(-) diff --git a/manifest b/manifest index b9cd930954..fd79919a70 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Add\snew\sassert()\sstatements\sdesigned\sto\sdetect\sand\sprevent\sthe\skind\nof\ssystem\sinterface\snumbering\serrors\sthat\shappened\sin\s[108691a3cb2f296f]\nand\sthat\swere\snot\sfixed\suntil\s[fe49703034bd23fa]. -D 2025-12-12T12:06:43.000 +C Get\sthe\s.testcase\sand\s.check\scommands\sworking\sin\sthe\sCLI. +D 2025-12-12T18:22:07.934 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -737,7 +737,7 @@ F src/random.c 606b00941a1d7dd09c381d3279a058d771f406c5213c9932bbd93d5587be4b9c F src/resolve.c 8d53771eb51a4ab5f970150c3a70969d8db79cd04a8774c2d296bbcf471a0dd0 F src/rowset.c 8432130e6c344b3401a8874c3cb49fefe6873fec593294de077afea2dce5ec97 F src/select.c 344518c1bba9c4636bf651b7642304abd2e7075ba35feb4bae42a51e5efe991f -F src/shell.c.in be8dc6e392cb16eb6b52cc950cc45d14bd1dc0f46aaa4eb33bb0f7b38e991205 +F src/shell.c.in f725bbdc4583c3728922b92281ccc44892c7b35bb665195ee7ca7d2374ecc7b4 F src/sqlite.h.in 706cacea5308b0244fb6cec92e08310fb427a125375c64137cc1f878ae4cf5c0 F src/sqlite3.rc 015537e6ac1eec6c7050e17b616c2ffe6f70fca241835a84a4f0d5937383c479 F src/sqlite3ext.h 5d5330f5f8461f5ce74960436ddcfa53ecd09c2b8b23901e22ae38aec3243998 @@ -1441,7 +1441,7 @@ F test/mmap4.test 2e2b4e32555b58da15176e6fe750f17c9dcf7f93 F test/mmapcorrupt.test 470fb44fe92e99c1d23701d156f8c17865f5b027063c9119dcfdb842791f4465 F test/mmapfault.test d4c9eff9cd8c2dc14bc43e71e042f175b0a26fe3 F test/mmapwarm.test 2272005969cd17a910077bd5082f70bc1fefad9a875afec7fc9af483898ecaf3 -F test/modeA.clitest aa4c12c45cf454a224258f42041677fbbd1c3d09e254900b8edbffc7c2b4c17f +F test/modeA.clitest 2db42f2814ba45d5e886fead881360e689bc27346cfc37e6bb46f63334bebc61 F test/multiplex.test d74c034e52805f6de8cc5432cef8c9eb774bb64ec29b83a22effc8ca4dac1f08 F test/multiplex2.test 580ca5817c7edbe4cc68fa150609c9473393003a F test/multiplex3.test fac575e0b1b852025575a6a8357701d80933e98b5d2fe6d35ddaa68f92f6a1f7 @@ -2184,8 +2184,8 @@ F tool/version-info.c 33d0390ef484b3b1cb685d59362be891ea162123cea181cb8e6d2cf6dd F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7 F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f -P fe49703034bd23fa8f43ffdf0e23ec9613688d957adf96b3f1ea86fb02a8d86b -R f52f4a9c387457edc2e18d724aa80ebc +P 4e6e551c89c4f18299b3d60369f439e80f2fe2b0e70199f649821b1dbf20c668 +R 45ff11e2e11683b7a57e94205e0a1b3b U drh -Z 9d4c0719badab3584588a1f120e6b6db +Z 269e8ff8cb00c409106cfe616c6f8090 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index d3ff44e01b..3750dbb604 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -4e6e551c89c4f18299b3d60369f439e80f2fe2b0e70199f649821b1dbf20c668 +1ca31e1a297c0d53d068afb658ce6602887cda0f3eaf42cd629c4d1b7204f0b0 diff --git a/src/shell.c.in b/src/shell.c.in index ccf3cd2c19..989024e873 100644 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -3682,8 +3682,8 @@ static const char *(azHelp[]) = { ".cd DIRECTORY Change the working directory to DIRECTORY", #endif ".changes on|off Show number of rows changed by SQL", + ".check OPTIONS ... Verify the results of a .testcase", #ifndef SQLITE_SHELL_FIDDLE - ".check GLOB Fail if output since .testcase does not match", ".clone NEWDB Clone data into NEWDB from the existing database", #endif ".connection [close] [#] Open or close an auxiliary database connection", @@ -3863,9 +3863,7 @@ static const char *(azHelp[]) = { ".system CMD ARGS... Run CMD ARGS... in a system shell", #endif ".tables ?TABLE? List names of tables matching LIKE pattern TABLE", -#ifndef SQLITE_SHELL_FIDDLE - ",testcase NAME Begin redirecting output to 'testcase-out.txt'", -#endif + ".testcase NAME Begin a test case.", ",testctrl CMD ... Run various sqlite3_test_control() operations", " Run \".testctrl\" with no arguments for details", ".timeout MS Try opening locked tables for MS milliseconds", @@ -8260,19 +8258,13 @@ static int dotCmdMode(ShellState *p){ ** launch a text editor when the redirection ends. ** --error-prefix X Use X as the left-margin prefix for error messages. ** Set to an empty string to restore the default. -** --glob GLOB Raise an error if the memory buffer does not match -** the GLOB pattern. -** --keep Continue using the same "memory" buffer. Do not -** reset it or delete it. Useful in combination with -** --glob, --not-glob, and/or --verify. -** ---notglob GLOB Raise an error if the memory buffer does not match -** the GLOB pattern. +** --keep Keep redirecting output to its current destination. +** Use this option in combination with --show or +** with --error-prefix when you do not want to stop +** a current redirection. ** --plain Use plain text rather than HTML tables with -w -** --show Write the memory buffer to the screen, for debugging. -** --verify ENDMARK Read subsequent lines of text until the first line -** that matches ENDMARK. Discard the ENDMARK. Compare -** the text against the accumulated output in memory and -** raise an error if there are any differences. +** --show Show output text captured by .testcase or by +** redirecting to "memory". ** -w Show the output in a web browser. Output is ** written into a temporary HTML file until the ** redirect ends, then the web browser is launched. @@ -8314,9 +8306,7 @@ static int dotCmdOutput(ShellState *p){ int eMode = 0; /* 0: .outout/.once, 'x'=.excel, 'w'=.www */ int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */ int bPlain = 0; /* --plain option */ - int bKeep = 0; /* --keep option */ - char *zCheck = 0; /* Argument to --glob, --notglob, --verify */ - int eCheck = 0; /* 1: --glob, 2: --notglob, 3: --verify */ + int bKeep = 0; /* Keep redirecting */ static const char *zBomUtf8 = "\357\273\277"; const char *zBom = 0; char c = azArg[0][0]; @@ -8342,32 +8332,17 @@ static int dotCmdOutput(ShellState *p){ bPlain = 1; }else if( c=='o' && z[0]=='1' && z[1]!=0 && z[2]==0 && (z[1]=='x' || z[1]=='e' || z[1]=='w') ){ - if( bKeep || eMode || eCheck ){ + if( bKeep || eMode ){ dotCmdError(p, i, "incompatible with prior options",0); goto dotCmdOutput_error; } eMode = z[1]; - }else if( cli_strcmp(z,"-keep")==0 ){ - bKeep = 1; }else if( cli_strcmp(z,"-show")==0 ){ if( cli_output_capture ){ sqlite3_fprintf(stdout, "%s", sqlite3_str_value(cli_output_capture)); } + }else if( cli_strcmp(z,"-keep")==0 ){ bKeep = 1; - }else if( cli_strcmp(z,"-glob")==0 - || cli_strcmp(z,"-notglob")==0 - || cli_strcmp(z,"-verify")==0 - ){ - if( eCheck || eMode ){ - dotCmdError(p, i, "incompatible with prior options",0); - goto dotCmdOutput_error; - } - if( i+1>=nArg ){ - dotCmdError(p, i, "missing argument", 0); - goto dotCmdOutput_error; - } - zCheck = azArg[++i]; - eCheck = z[1]=='g' ? 1 : z[1]=='n' ? 2 : 3; }else if( optionMatch(z,"error-prefix") ){ if( i+1>=nArg ){ dotCmdError(p, i, "missing argument", 0); @@ -8382,7 +8357,7 @@ static int dotCmdOutput(ShellState *p){ return 1; } }else if( zFile==0 && eMode==0 ){ - if( bKeep || eCheck ){ + if( bKeep ){ dotCmdError(p, i, "incompatible with prior options",0); goto dotCmdOutput_error; } @@ -8418,49 +8393,6 @@ static int dotCmdOutput(ShellState *p){ }else{ p->nPopOutput = 0; } - if( eCheck ){ - char *zTest; - if( cli_output_capture ){ - zTest = sqlite3_str_value(cli_output_capture); - }else{ - zTest = ""; - } - p->nTestRun++; - if( eCheck==3 ){ - int nCheck = strlen30(zCheck); - sqlite3_str *pPattern = sqlite3_str_new(p->db); - char *zPattern; - sqlite3_int64 iStart = p->lineno; - char zLine[2000]; - while( sqlite3_fgets(zLine,sizeof(zLine),p->in) ){ - if( strchr(zLine,'\n') ) p->lineno++; - if( cli_strncmp(zCheck,zLine,nCheck)==0 ) break; - sqlite3_str_appendall(pPattern, zLine); - } - zPattern = sqlite3_str_finish(pPattern); - if( cli_strcmp(zPattern,zTest)!=0 ){ - sqlite3_fprintf(stderr, - "%s:%lld: --verify does matches prior output\n", - p->zInFile, iStart); - p->nTestErr++; - } - sqlite3_free(zPattern); - }else{ - char *zGlob = sqlite3_mprintf("*%s*", zCheck); - if( eCheck==1 && sqlite3_strglob(zGlob, zTest)!=0 ){ - sqlite3_fprintf(stderr, - "%s:%lld: --glob \"%s\" does not match prior output\n", - p->zInFile, p->lineno, zCheck); - p->nTestErr++; - }else if( eCheck==2 && sqlite3_strglob(zGlob, zTest)==0 ){ - sqlite3_fprintf(stderr, - "%s:%lld: --notglob \"%s\" matches prior output\n", - p->zInFile, p->lineno, zCheck); - p->nTestErr++; - } - sqlite3_free(zGlob); - } - } if( !bKeep ) output_reset(p); #ifndef SQLITE_NOHAVE_SYSTEM if( eMode=='e' || eMode=='x' || eMode=='w' ){ @@ -8542,6 +8474,176 @@ dotCmdOutput_error: return 1; } +/* +** DOT-COMMAND: .check +** USAGE: .check [OPTIONS] PATTERN +** +** Verify results of commands since the most recent .testcase command. +** Restore output to the console, unless --keep is used. +** +** If PATTERN starts with "<dot.nArg; /* Number of arguments */ + char **azArg = p->dot.azArg; /* Text of the arguments */ + int i; /* Loop counter */ + int k; /* Result of pickStr() */ + char *zTest; /* Textcase result */ + int bKeep = 0; /* --keep option */ + char *zCheck = 0; /* PATTERN argument */ + char *zPattern = 0; /* Actual test pattern */ + int eCheck = 0; /* 1: --glob, 2: --notglob, 3: --exact */ + int isOk; /* True if results are OK */ + sqlite3_int64 iStart = p->lineno; /* Line number of .check statement */ + + if( p->zTestcase[0]==0 ){ + dotCmdError(p, 0, "no .testcase is active", 0); + return 1; + } + for(i=1; i=0 + ){ + if( eCheck && eCheck!=k+1 ){ + dotCmdError(p, i, "incompatible with prior options",0); + return 1; + } + eCheck = k+1; + }else if( zCheck ){ + dotCmdError(p, i, "unknown option", 0); + return 1; + }else{ + zCheck = azArg[i]; + } + } + if( zCheck==0 ){ + dotCmdError(p, 0, "no PATTERN specified", 0); + return 1; + } + if( cli_output_capture ){ + zTest = sqlite3_str_value(cli_output_capture); + shell_check_oom(zTest); + }else{ + zTest = ""; + } + p->nTestRun++; + if( zCheck[0]=='<' && zCheck[1]=='<' && zCheck[2]!=0 ){ + int nCheck = strlen30(zCheck); + sqlite3_str *pPattern = sqlite3_str_new(p->db); + char zLine[2000]; + while( sqlite3_fgets(zLine,sizeof(zLine),p->in) ){ + if( strchr(zLine,'\n') ) p->lineno++; + if( cli_strncmp(&zCheck[2],zLine,nCheck-2)==0 ) break; + sqlite3_str_appendall(pPattern, zLine); + } + zPattern = sqlite3_str_finish(pPattern); + }else{ + zPattern = zCheck; + } + switch( eCheck ){ + case 1: { + char *zGlob = sqlite3_mprintf("*%s*", zPattern); + isOk = testcase_glob(zGlob, zTest)!=0; + sqlite3_free(zGlob); + break; + } + case 2: { + char *zGlob = sqlite3_mprintf("*%s*", zPattern); + isOk = testcase_glob(zGlob, zTest)==0; + sqlite3_free(zGlob); + break; + } + default: { + isOk = cli_strcmp(zPattern,zTest)==0; + break; + } + } + if( !isOk ){ + sqlite3_fprintf(stderr, + "%s:%lld: .check failed for testcase %s\n", + p->zInFile, iStart, p->zTestcase); + p->nTestErr++; + sqlite3_fprintf(stderr, "Expected: [%s]\n", zPattern); + sqlite3_fprintf(stderr, "Got: [%s]\n", zTest); + } + if( zPattern!=zCheck ){ + sqlite3_free(zPattern); + } + if( !bKeep ){ + output_reset(p); + p->zTestcase[0] = 0; + } + return 0; +} + +/* +** DOT-COMMAND: .testcase +** USAGE: .testcase [OPTIONS] NAME +** +** Start a new test case identified by NAME. All output +** through the next ".check" command is captured for comparison. See the +** ".check" commandn for additional informatioon. +** +** Options: +** --error-prefix TEXT Change error message prefix text to TEXT +*/ +static int dotCmdTestcase(ShellState *p){ + int nArg = p->dot.nArg; /* Number of arguments */ + char **azArg = p->dot.azArg; /* Text of the arguments */ + int i; /* Loop counter */ + const char *zName = 0; /* Testcase name */ + + for(i=1; i=nArg ){ + dotCmdError(p, i, "missing argument", 0); + return 1; + } + free(p->zErrPrefix); + i++; + p->zErrPrefix = azArg[i][0]==0 ? 0 : strdup(azArg[i]); + }else if( zName ){ + dotCmdError(p, i, "unknown option", 0); + return 1; + }else{ + zName = azArg[i]; + } + } + output_reset(p); + if( cli_output_capture ){ + sqlite3_str_free(cli_output_capture); + } + cli_output_capture = sqlite3_str_new(0); + if( zName ){ + sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "%s", zName); + }else{ + sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "%s:%lld", + p->zInFile, p->lineno); + } + return 0; +} + /* ** Enlarge the space allocated in p->dot so that it can hold more ** than nArg parsed command-line arguments. @@ -8793,31 +8895,13 @@ static int do_meta_command(const char *zLine, ShellState *p){ } }else -#ifndef SQLITE_SHELL_FIDDLE /* Cancel output redirection, if it is currently set (by .testcase) ** Then read the content of the testcase-out.txt file and compare against ** azArg[1]. If there are differences, report an error and exit. */ if( c=='c' && n>=3 && cli_strncmp(azArg[0], "check", n)==0 ){ - char *zRes = 0; - output_reset(p); - if( nArg!=2 ){ - eputz("Usage: .check GLOB-PATTERN\n"); - rc = 2; - }else if( (zRes = readFile("testcase-out.txt", 0))==0 ){ - rc = 2; - }else if( testcase_glob(azArg[1],zRes)==0 ){ - cli_printf(stderr, - "testcase-%s FAILED\n Expected: [%s]\n Got: [%s]\n", - p->zTestcase, azArg[1], zRes); - rc = 1; - }else{ - cli_printf(p->out, "testcase-%s ok\n", p->zTestcase); - p->nCheck++; - } - sqlite3_free(zRes); + rc = dotCmdCheck(p); }else -#endif /* !defined(SQLITE_SHELL_FIDDLE) */ #ifndef SQLITE_SHELL_FIDDLE if( c=='c' && cli_strncmp(azArg[0], "clone", n)==0 ){ @@ -10949,21 +11033,11 @@ static int do_meta_command(const char *zLine, ShellState *p){ if( rc ) return shellDatabaseError(p->db); }else -#ifndef SQLITE_SHELL_FIDDLE - /* Begin redirecting output to the file "testcase-out.txt" */ + /* Set the p->zTestcase name and begin redirecting output into + ** the cli_output_capture sqlite3_str */ if( c=='t' && cli_strcmp(azArg[0],"testcase")==0 ){ - output_reset(p); - p->out = output_file_open(p, "testcase-out.txt"); - if( p->out==0 ){ - eputz("Error: cannot open 'testcase-out.txt'\n"); - } - if( nArg>=2 ){ - sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "%s", azArg[1]); - }else{ - sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "?"); - } + rc = dotCmdTestcase(p); }else -#endif /* !defined(SQLITE_SHELL_FIDDLE) */ #ifndef SQLITE_UNTESTABLE if( c=='t' && n>=8 && cli_strncmp(azArg[0], "testctrl", n)==0 ){ diff --git a/test/modeA.clitest b/test/modeA.clitest index d5f2185bdc..2dcbf0c45c 100644 --- a/test/modeA.clitest +++ b/test/modeA.clitest @@ -21,9 +21,9 @@ INSERT INTO t1 SELECT b,c,d,e,a FROM t1; INSERT INTO t1 SELECT d,e,a,b,c FROM t1; .mode box -.output memory +.testcase 100 SELECT * FROM t1; -.output --verify END +.check <