From 92d4d7a92e1a1e465a0d5fd3e9a42e90ddcbda4b Mon Sep 17 00:00:00 2001 From: danielk1977 Date: Fri, 4 May 2007 12:05:56 +0000 Subject: [PATCH] Test cases and corrections to IO and malloc() error handling in incremental blob IO functions. (CVS 3915) FossilOrigin-Name: 641e55284e1ba6070073c83ac6ed78ffb29f7e60 --- manifest | 23 ++--- manifest.uuid | 2 +- src/btree.c | 65 +++++++------- src/tclsqlite.c | 11 ++- src/vdbeaux.c | 14 +-- src/vdbeblob.c | 36 +++++--- test/incrblob_err.test | 192 +++++++++++++++++++++++++++++++++++++++++ test/tester.tcl | 5 +- 8 files changed, 277 insertions(+), 71 deletions(-) create mode 100644 test/incrblob_err.test diff --git a/manifest b/manifest index 0152f1b0e7..ba3e556ae2 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sa\sformatting\serrror\sin\sI/O\slogging.\s(CVS\s3914) -D 2007-05-04T12:01:03 +C Test\scases\sand\scorrections\sto\sIO\sand\smalloc()\serror\shandling\sin\sincremental\sblob\sIO\sfunctions.\s(CVS\s3915) +D 2007-05-04T12:05:56 F Makefile.in 8cab54f7c9f5af8f22fd97ddf1ecfd1e1860de62 F Makefile.linux-gcc 2d8574d1ba75f129aba2019f0b959db380a90935 F README 9c4e2d6706bdcc3efdd773ce752a8cdab4f90028 @@ -59,7 +59,7 @@ F src/alter.c 2c79ec40f65e33deaf90ca493422c74586e481a3 F src/analyze.c 4bbf5ddf9680587c6d4917e02e378b6037be3651 F src/attach.c a16ada4a4654a0d126b8223ec9494ebb81bc5c3c F src/auth.c 902f4722661c796b97f007d9606bd7529c02597f -F src/btree.c b88c9265e323b9a55290c39f9712e44050a6162c +F src/btree.c a47fe8172eb62468f78a2fe4904d2e6b050038fb F src/btree.h 2c187d60cf76d74c2b4767294d6b5fa267037ff0 F src/build.c 02e01ec7907c7d947ab3041fda0e81eaed05db42 F src/callback.c 6414ed32d55859d0f65067aa5b88d2da27b3af9e @@ -101,7 +101,7 @@ F src/sqlite.h.in a666300976897eced975b448f722a722b362c6b1 F src/sqlite3ext.h 7d0d363ea7327e817ef0dfe1b7eee1f171b72890 F src/sqliteInt.h 5a8c0221a4f11998f46aa76364a9559af8d7c1f7 F src/table.c 6d0da66dde26ee75614ed8f584a1996467088d06 -F src/tclsqlite.c dde509871614d17f8ab5f3b4bc496b0af07280c7 +F src/tclsqlite.c 0906902b96e4b292ba98499dd380e22d3322f817 F src/test1.c 29a39fdde51f4612082ecf3f5af54dac93766f87 F src/test2.c 24458b17ab2f3c90cbc1c8446bd7ffe69be62f88 F src/test3.c 946ea9d1a8c928656e3c70f0a2fcb8e733a15e86 @@ -129,8 +129,8 @@ F src/vdbe.c a4abf744b5376372a9be30f02ab4b231f353cab1 F src/vdbe.h 0025259af1939fb264a545816c69e4b5b8d52691 F src/vdbeInt.h cb02cbbceddf3b40d49012e9f41576f17bcbec97 F src/vdbeapi.c 37d793559390bec8a00c556f651f21b5f9e589af -F src/vdbeaux.c 51acaab4275b5fddc7af5e7d1d2594044216ac46 -F src/vdbeblob.c ed2f9b46cc2de8de97d2a4a4ec466c5914d68333 +F src/vdbeaux.c 966d166f9d4079552c7cb48855afe8c1aa331556 +F src/vdbeblob.c 58b3f68f6bc18b58aeab3a31cff083cb4127292f F src/vdbefifo.c 3ca8049c561d5d67cbcb94dc909ae9bb68c0bf8f F src/vdbemem.c ba98f8572ec4609846b368fa7580db178022f1bb F src/vtab.c 89a0d5f39c1beba65a77fdb4d507b831fc5e6baf @@ -243,6 +243,7 @@ F test/func.test 6727c7729472ae52b5acd86e802f89aa350ba50f F test/hook.test 7e7645fd9a033f79cce8fdff151e32715e7ec50a F test/in.test 369cb2aa1eab02296b4ec470732fe8c131260b1d F test/incrblob.test 5cf5a7693c6cde33eef361d94c00c7bdbc30f563 +F test/incrblob_err.test 9f78c159279c992fa5ce49c06f50b680fc470520 F test/incrvacuum.test 2173bc075c7b3b96ccf228d737dd4f5c29500dc4 F test/incrvacuum_ioerr.test 0ebc382bcc2036ec58cf49cc5ffada45f75d907b F test/index.test e65df12bed94b2903ee89987115e1578687e9266 @@ -336,7 +337,7 @@ F test/table.test dbdfd06aef054ad5aed8e57a782137d57d5c5528 F test/tableapi.test 036575a98dcce7c92e9f39056839bbad8a715412 F test/tclsqlite.test 726c301d35a2c1f4181fb772a607f785dd9e284e F test/temptable.test c36f3e5a94507abb64f7ba23deeb4e1a8a8c3821 -F test/tester.tcl dcebe3c5bf15f3b4ba015b4b2237030c1e384941 +F test/tester.tcl 9382df472e0e86cbfddc44ab8c8cc02497bc9c8a F test/thread1.test 776c9e459b75ba905193b351926ac4019b049f35 F test/thread2.test 6d7b30102d600f51b4055ee3a5a19228799049fb F test/threadtest1.c 6029d9c5567db28e6dc908a0c63099c3ba6c383b @@ -473,7 +474,7 @@ F www/tclsqlite.tcl bb0d1357328a42b1993d78573e587c6dcbc964b9 F www/vdbe.tcl 87a31ace769f20d3627a64fa1fade7fed47b90d0 F www/version3.tcl 890248cf7b70e60c383b0e84d77d5132b3ead42b F www/whentouse.tcl fc46eae081251c3c181bd79c5faef8195d7991a5 -P 2a178d0c7950c9d403c0bc43c2043de945fb24e0 -R 2c92eb8d26a624968d9428e7829962d0 -U drh -Z f76b8b0273f8e85ef3b644774d94b3bf +P 2d37687a08a7b6b3a7f77f55a7c2e29a1f8731a8 +R 631ed7d60317b63e0f0649096fcada2a +U danielk1977 +Z dfc7741fad7b57c2866d720631f3b47d diff --git a/manifest.uuid b/manifest.uuid index 32c047af01..f9069a3c1f 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -2d37687a08a7b6b3a7f77f55a7c2e29a1f8731a8 \ No newline at end of file +641e55284e1ba6070073c83ac6ed78ffb29f7e60 \ No newline at end of file diff --git a/src/btree.c b/src/btree.c index c517c9e0c5..7a786602e3 100644 --- a/src/btree.c +++ b/src/btree.c @@ -9,7 +9,7 @@ ** May you share freely, never taking more than you give. ** ************************************************************************* -** $Id: btree.c,v 1.371 2007/05/04 08:32:14 danielk1977 Exp $ +** $Id: btree.c,v 1.372 2007/05/04 12:05:56 danielk1977 Exp $ ** ** This file implements a external (disk-based) database using BTrees. ** For a detailed discussion of BTrees, refer to @@ -656,6 +656,30 @@ static void unlockAllTables(Btree *p){ static void releasePage(MemPage *pPage); /* Forward reference */ +#ifndef SQLITE_OMIT_INCRBLOB +/* +** Invalidate the overflow page-list cache for cursor pCur, if any. +*/ +static void invalidateOverflowCache(BtCursor *pCur){ + sqliteFree(pCur->aOverflow); + pCur->aOverflow = 0; +} + +/* +** Invalidate the overflow page-list cache for all cursors opened +** on the shared btree structure pBt. +*/ +static void invalidateAllOverflowCache(BtShared *pBt){ + BtCursor *p; + for(p=pBt->pCursor; p; p=p->pNext){ + invalidateOverflowCache(p); + } +} +#else + #define invalidateOverflowCache(x) + #define invalidateAllOverflowCache(x) +#endif + /* ** Save the current cursor position in the variables BtCursor.nKey ** and BtCursor.pKey. The cursor's state is set to CURSOR_REQUIRESEEK. @@ -695,12 +719,7 @@ static int saveCursorPosition(BtCursor *pCur){ pCur->eState = CURSOR_REQUIRESEEK; } -#ifndef SQLITE_OMIT_INCRBLOB - /* Delete the cache of overflow page numbers. */ - sqliteFree(pCur->aOverflow); - pCur->aOverflow = 0; -#endif - + invalidateOverflowCache(pCur); return rc; } @@ -2423,19 +2442,11 @@ static int incrVacuumStep(BtShared *pBt, Pgno nFin){ */ int sqlite3BtreeIncrVacuum(Btree *p){ BtShared *pBt = p->pBt; - BtCursor *pCur; - assert( pBt->inTransaction==TRANS_WRITE && p->inTrans==TRANS_WRITE ); if( !pBt->autoVacuum ){ return SQLITE_DONE; } -#ifndef SQLITE_OMIT_INCRBLOB - for(pCur=pBt->pCursor; pCur; pCur=pCur->pNext){ - /* Delete the cache of overflow page numbers. */ - sqliteFree(pCur->aOverflow); - pCur->aOverflow = 0; - } -#endif + invalidateAllOverflowCache(pBt); return incrVacuumStep(pBt, 0); } @@ -2455,15 +2466,7 @@ static int autoVacuumCommit(BtShared *pBt, Pgno *pnTrunc){ int nRef = sqlite3PagerRefcount(pPager); #endif -#ifndef SQLITE_OMIT_INCRBLOB - BtCursor *pCur; - for(pCur=pBt->pCursor; pCur; pCur=pCur->pNext){ - /* Delete the cache of overflow page numbers. */ - sqliteFree(pCur->aOverflow); - pCur->aOverflow = 0; - } -#endif - + invalidateAllOverflowCache(pBt); assert(pBt->autoVacuum); if( !pBt->incrVacuum ){ Pgno nFin = 0; @@ -2959,9 +2962,7 @@ int sqlite3BtreeCloseCursor(BtCursor *pCur){ } releasePage(pCur->pPage); unlockBtreeIfUnused(pBt); -#ifndef SQLITE_OMIT_INCRBLOB - sqliteFree(pCur->aOverflow); -#endif + invalidateOverflowCache(pCur); sqliteFree(pCur); return SQLITE_OK; } @@ -5801,18 +5802,12 @@ int sqlite3BtreeCreateTable(Btree *p, int *piTable, int flags){ Pgno pgnoMove; /* Move a page here to make room for the root-page */ MemPage *pPageMove; /* The page to move to. */ -#ifndef SQLITE_OMIT_INCRBLOB /* Creating a new table may probably require moving an existing database ** to make room for the new tables root page. In case this page turns ** out to be an overflow page, delete all overflow page-map caches ** held by open cursors. */ - BtCursor *pCur; - for(pCur=pBt->pCursor; pCur; pCur=pCur->pNext){ - sqliteFree(pCur->aOverflow); - pCur->aOverflow = 0; - } -#endif + invalidateAllOverflowCache(pBt); /* Read the value of meta[3] from the database to determine where the ** root page of the new table should go. meta[3] is the largest root-page diff --git a/src/tclsqlite.c b/src/tclsqlite.c index 1a2567e8cb..79429cb168 100644 --- a/src/tclsqlite.c +++ b/src/tclsqlite.c @@ -12,7 +12,7 @@ ** A TCL Interface to SQLite. Append this file to sqlite3.c and ** compile the whole thing to build a TCL-enabled version of SQLite. ** -** $Id: tclsqlite.c,v 1.182 2007/05/03 16:31:26 danielk1977 Exp $ +** $Id: tclsqlite.c,v 1.183 2007/05/04 12:05:56 danielk1977 Exp $ */ #include "tcl.h" #include @@ -153,7 +153,8 @@ static void closeIncrblobChannels(SqliteDb *pDb){ */ static int incrblobClose(ClientData instanceData, Tcl_Interp *interp){ IncrblobChannel *p = (IncrblobChannel *)instanceData; - sqlite3_blob_close(p->pBlob); + int rc = sqlite3_blob_close(p->pBlob); + sqlite3 *db = p->pDb->db; /* Remove the channel from the SqliteDb.pIncrblob list. */ if( p->pNext ){ @@ -166,7 +167,13 @@ static int incrblobClose(ClientData instanceData, Tcl_Interp *interp){ p->pDb->pIncrblob = p->pNext; } + /* Free the IncrblobChannel structure */ Tcl_Free((char *)p); + + if( rc!=SQLITE_OK ){ + Tcl_SetResult(interp, (char *)sqlite3_errmsg(db), TCL_VOLATILE); + return TCL_ERROR; + } return TCL_OK; } diff --git a/src/vdbeaux.c b/src/vdbeaux.c index 83fe3e9fe1..7cc44306ef 100644 --- a/src/vdbeaux.c +++ b/src/vdbeaux.c @@ -464,12 +464,14 @@ static void freeP3(int p3type, void *p3){ ** Change N opcodes starting at addr to No-ops. */ void sqlite3VdbeChangeToNoop(Vdbe *p, int addr, int N){ - VdbeOp *pOp = &p->aOp[addr]; - while( N-- ){ - freeP3(pOp->p3type, pOp->p3); - memset(pOp, 0, sizeof(pOp[0])); - pOp->opcode = OP_Noop; - pOp++; + if( p && p->aOp ){ + VdbeOp *pOp = &p->aOp[addr]; + while( N-- ){ + freeP3(pOp->p3type, pOp->p3); + memset(pOp, 0, sizeof(pOp[0])); + pOp->opcode = OP_Noop; + pOp++; + } } } diff --git a/src/vdbeblob.c b/src/vdbeblob.c index c81ac21410..9fbdca71a1 100644 --- a/src/vdbeblob.c +++ b/src/vdbeblob.c @@ -10,7 +10,7 @@ ** ************************************************************************* ** -** $Id: vdbeblob.c,v 1.5 2007/05/03 18:14:10 danielk1977 Exp $ +** $Id: vdbeblob.c,v 1.6 2007/05/04 12:05:56 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -170,12 +170,14 @@ int sqlite3_blob_open( ** and offset cache without causing any IO. */ sqlite3VdbeChangeP2(v, 5, pTab->nCol+1); - sqlite3VdbeMakeReady(v, 1, 0, 1, 0); + if( !sqlite3MallocFailed() ){ + sqlite3VdbeMakeReady(v, 1, 0, 1, 0); + } } rc = sqlite3SafetyOff(db); - if( rc!=SQLITE_OK ){ - return rc; + if( rc!=SQLITE_OK || sqlite3MallocFailed() ){ + goto blob_open_out; } sqlite3_bind_int64((sqlite3_stmt *)v, 1, iRow); @@ -222,11 +224,11 @@ int sqlite3_blob_open( } blob_open_out: + zErr[sizeof(zErr)-1] = '\0'; if( rc!=SQLITE_OK || sqlite3MallocFailed() ){ sqlite3_finalize((sqlite3_stmt *)v); } - zErr[sizeof(zErr)-1] = '\0'; - sqlite3Error(db, rc, zErr); + sqlite3Error(db, rc, (rc==SQLITE_OK?0:zErr)); return sqlite3ApiExit(db, rc); } @@ -235,31 +237,37 @@ blob_open_out: */ int sqlite3_blob_close(sqlite3_blob *pBlob){ Incrblob *p = (Incrblob *)pBlob; - sqlite3_finalize(p->pStmt); + sqlite3_stmt *pStmt = p->pStmt; sqliteFree(p); - return SQLITE_OK; + return sqlite3_finalize(pStmt); } /* ** Read data from a blob handle. */ int sqlite3_blob_read(sqlite3_blob *pBlob, void *z, int n, int iOffset){ + int rc = SQLITE_ERROR; Incrblob *p = (Incrblob *)pBlob; - if( (iOffset+n)>p->nByte ){ - return SQLITE_ERROR; + Vdbe *v = (Vdbe *)(p->pStmt); + if( (iOffset+n)<=p->nByte ){ + rc = sqlite3BtreeData(p->pCsr, iOffset+p->iOffset, n, z); } - return sqlite3BtreeData(p->pCsr, iOffset+p->iOffset, n, z); + v->rc = rc; + return sqlite3ApiExit(v->db, v->rc); } /* ** Write data to a blob handle. */ int sqlite3_blob_write(sqlite3_blob *pBlob, const void *z, int n, int iOffset){ + int rc = SQLITE_ERROR; Incrblob *p = (Incrblob *)pBlob; - if( (iOffset+n)>p->nByte ){ - return SQLITE_ERROR; + Vdbe *v = (Vdbe *)(p->pStmt); + if( (iOffset+n)<=p->nByte ){ + rc = sqlite3BtreePutData(p->pCsr, iOffset+p->iOffset, n, z); } - return sqlite3BtreePutData(p->pCsr, iOffset+p->iOffset, n, z); + v->rc = rc; + return sqlite3ApiExit(v->db, v->rc); } /* diff --git a/test/incrblob_err.test b/test/incrblob_err.test new file mode 100644 index 0000000000..69a0d7f612 --- /dev/null +++ b/test/incrblob_err.test @@ -0,0 +1,192 @@ +# 2007 May 1 +# +# 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. +# +#*********************************************************************** +# +# $Id: incrblob_err.test,v 1.1 2007/05/04 12:05:56 danielk1977 Exp $ +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +# Usage: do_malloc_test +# +# The first argument, , is an integer used to name the +# tests executed by this proc. Options are as follows: +# +# -tclprep TCL script to run to prepare test. +# -sqlprep SQL script to run to prepare test. +# -tclbody TCL script to run with malloc failure simulation. +# -sqlbody TCL script to run with malloc failure simulation. +# -cleanup TCL script to run after the test. +# +# This command runs a series of tests to verify SQLite's ability +# to handle an out-of-memory condition gracefully. It is assumed +# that if this condition occurs a malloc() call will return a +# NULL pointer. Linux, for example, doesn't do that by default. See +# the "BUGS" section of malloc(3). +# +# Each iteration of a loop, the TCL commands in any argument passed +# to the -tclbody switch, followed by the SQL commands in any argument +# passed to the -sqlbody switch are executed. Each iteration the +# Nth call to sqliteMalloc() is made to fail, where N is increased +# each time the loop runs starting from 1. When all commands execute +# successfully, the loop ends. +# +proc do_malloc_test {tn args} { + array unset ::mallocopts + array set ::mallocopts $args + + set ::go 1 + for {set ::n 1} {$::go && $::n < 50000} {incr ::n} { + do_test incrblob_err-$tn.$::n { + + # Remove all traces of database files test.db and test2.db from the files + # system. Then open (empty database) "test.db" with the handle [db]. + # + sqlite_malloc_fail 0 + catch {db close} + catch {file delete -force test.db} + catch {file delete -force test.db-journal} + catch {file delete -force test2.db} + catch {file delete -force test2.db-journal} + catch {sqlite3 db test.db} + set ::DB [sqlite3_connection_pointer db] + + # Execute any -tclprep and -sqlprep scripts. + # + if {[info exists ::mallocopts(-tclprep)]} { + eval $::mallocopts(-tclprep) + } + if {[info exists ::mallocopts(-sqlprep)]} { + execsql $::mallocopts(-sqlprep) + } + + # Now set the ${::n}th malloc() to fail and execute the -tclbody and + # -sqlbody scripts. + # + sqlite_malloc_fail $::n + set ::mallocbody {} + if {[info exists ::mallocopts(-tclbody)]} { + append ::mallocbody "$::mallocopts(-tclbody)\n" + } + if {[info exists ::mallocopts(-sqlbody)]} { + append ::mallocbody "db eval {$::mallocopts(-sqlbody)}" + } + set v [catch $::mallocbody msg] + + # If the test fails (if $v!=0) and the database connection actually + # exists, make sure the failure code is SQLITE_NOMEM. + if {$v && [info command db]=="db" && [info exists ::mallocopts(-sqlbody)] + && [db errorcode]!=7} { + set v 999 + } + + set leftover [lindex [sqlite_malloc_stat] 2] + if {$leftover>0} { + if {$leftover>1} {puts "\nLeftover: $leftover\nReturn=$v Message=$msg"} + set ::go 0 + if {$v} { + puts "\nError message returned: $msg" + } else { + set v {1 1} + } + } else { + set v2 [expr {$msg=="" || $msg=="out of memory"}] + if {!$v2} {puts "\nError message returned: $msg"} + lappend v $v2 + } + } {1 1} + + if {[info exists ::mallocopts(-cleanup)]} { + catch [list uplevel #0 $::mallocopts(-cleanup)] msg + } + } + unset ::mallocopts +} + +set ::fd [open [info script]] +set ::data [read $::fd] +close $::fd + +do_malloc_test 1 -tclprep { + set bytes [file size [info script]] + execsql { + CREATE TABLE blobs(k, v BLOB); + INSERT INTO blobs VALUES(1, zeroblob($::bytes)); + } +} -tclbody { + set ::blob [db incrblob blobs v 1] + set rc [catch {puts -nonewline $::blob $::data}] + if {$rc} { error "out of memory" } +} + +do_malloc_test 2 -tclprep { + execsql { + CREATE TABLE blobs(k, v BLOB); + INSERT INTO blobs VALUES(1, $::data); + } +} -tclbody { + set ::blob [db incrblob blobs v 1] + set rc [catch {set ::r [read $::blob]}] + if {$rc} { + error "out of memory" + } elseif {$::r ne $::data} { + error "Bad data read..." + } +} + +do_malloc_test 3 -tclprep { + execsql { + CREATE TABLE blobs(k, v BLOB); + INSERT INTO blobs VALUES(1, $::data); + } +} -tclbody { + set ::blob [db incrblob blobs v 1] + set rc [catch {set ::r [read $::blob]}] + if {$rc} { + error "out of memory" + } elseif {$::r ne $::data} { + error "Bad data read..." + } + set rc [catch {close $::blob}] + if {$rc} { + error "out of memory" + } +} +sqlite_malloc_fail 0 + +do_ioerr_test incrblob_err-4 -cksum 1 -sqlprep { + CREATE TABLE blobs(k, v BLOB); + INSERT INTO blobs VALUES(1, $::data); +} -tclbody { + set ::blob [db incrblob blobs v 1] + read $::blob +} + +do_ioerr_test incrblob_err-5 -cksum 1 -sqlprep { + CREATE TABLE blobs(k, v BLOB); + INSERT INTO blobs VALUES(1, zeroblob(length(CAST($::data AS BLOB)))); +} -tclbody { + set ::blob [db incrblob blobs v 1] + puts -nonewline $::blob $::data + close $::blob +} + +do_ioerr_test incrblob_err-6 -cksum 1 -sqlprep { + CREATE TABLE blobs(k, v BLOB); + INSERT INTO blobs VALUES(1, $::data || $::data || $::data); +} -tclbody { + set ::blob [db incrblob blobs v 1] + seek $::blob -20 end + puts -nonewline $::blob "12345678900987654321" + close $::blob +} + +finish_test diff --git a/test/tester.tcl b/test/tester.tcl index aed7446719..b35c634f03 100644 --- a/test/tester.tcl +++ b/test/tester.tcl @@ -11,7 +11,7 @@ # This file implements some common TCL routines used for regression # testing the SQLite library # -# $Id: tester.tcl,v 1.80 2007/04/28 15:47:45 danielk1977 Exp $ +# $Id: tester.tcl,v 1.81 2007/05/04 12:05:56 danielk1977 Exp $ # Make sure tclsqlite3 was compiled correctly. Abort now with an # error message if not. @@ -399,7 +399,7 @@ proc do_ioerr_test {testname args} { set ::go 1 for {set n $::ioerropts(-start)} {$::go} {incr n} { -set ::TN $n + set ::TN $n incr ::ioerropts(-count) -1 if {$::ioerropts(-count)<0} break @@ -480,6 +480,7 @@ set ::TN $n # 1. We never hit the IO error and the SQL returned OK # 2. An IO error was hit and the SQL failed # +#puts "$s $r $::go - $msg" expr { ($s && !$r && !$::go) || (!$s && $r && $::go) } } {1}