]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add test/wapptest.tcl, a wapp alternative to releasetest.tcl.
authordan <dan@noemail.net>
Tue, 9 Apr 2019 19:53:32 +0000 (19:53 +0000)
committerdan <dan@noemail.net>
Tue, 9 Apr 2019 19:53:32 +0000 (19:53 +0000)
FossilOrigin-Name: a4af0c2fee05aaa2e95ae6a5c847ff2d363e24f325f4ffdcf51bc264b9bf5e2d

manifest
manifest.uuid
test/releasetest_data.tcl [new file with mode: 0644]
test/wapp.tcl [new file with mode: 0644]
test/wapptest.tcl [new file with mode: 0755]

index 7098743fed1de9f2d6f841486edcd25c8b11a6e4..bed37db08025964a1c9bf6ca291267b1ef63596a 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Make\sthe\stestcase()\smacro\sadded\sin\sthe\sprevious\scheck-in\sreachable\sfor\ntesting.
-D 2019-04-07T18:21:12.384
+C Add\stest/wapptest.tcl,\sa\swapp\salternative\sto\sreleasetest.tcl.
+D 2019-04-09T19:53:32.352
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -1227,6 +1227,7 @@ F test/regexp1.test 497ea812f264d12b6198d6e50a76be4a1973a9d8
 F test/regexp2.test 40e894223b3d6672655481493f1be12012f2b33c
 F test/reindex.test 44edd3966b474468b823d481eafef0c305022254
 F test/releasetest.tcl 7712811e0f4e2f198ec786cb2e1352b3793d7395f48a3cceef0572d8823eb75e x
+F test/releasetest_data.tcl 1a89107e0f3be09efa9819367ffd96dbe9b82d571c03a75ba19444ca2432d05e
 F test/resetdb.test 8062cf10a09d8c048f8de7711e94571c38b38168db0e5877ba7561789e5eeb2b
 F test/resolver01.test f4022acafda7f4d40eca94dbf16bc5fc4ac30ceb
 F test/rollback.test 06680159bc6746d0f26276e339e3ae2f951c64812468308838e0a3362d911eaa
@@ -1649,6 +1650,8 @@ F test/walshared.test 0befc811dcf0b287efae21612304d15576e35417
 F test/walslow.test c05c68d4dc2700a982f89133ce103a1a84cc285f
 F test/walthread.test 14b20fcfa6ae152f5d8e12f5dc8a8a724b7ef189f5d8ef1e2ceab79f2af51747
 F test/walvfs.test c0faffda13d045a96dfc541347886bb1a3d6f3205857fc98e683edfab766ea88
+F test/wapp.tcl b440cd8cf57953d3a49e7ee81e6a18f18efdaf113b69f7d8482b0710a64566ec
+F test/wapptest.tcl 2475dd60ac518bedb9c1021e9fdeaa74f4356dd44ca569328b9e91e16a85f95e x
 F test/where.test 0607caa5a1fbfe7b93b95705981b463a3a0408038f22ae6e9dc11b36902b0e95
 F test/where2.test 478d2170637b9211f593120648858593bf2445a1
 F test/where3.test 2341a294e17193a6b1699ea7f192124a5286ca6acfcc3f4b06d16c931fbcda2c
@@ -1814,7 +1817,10 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P df58774e994bd306b1a2e1f259e7e4408f01c5b1dc104673698168bbf8a63ce5
-R 8f225a384e99d5cc8fcbc3b1a5a25930
-U drh
-Z 42970063c3d79b8d4bcc406c6a540b2c
+P 80704a16f6dbbeacc65fa36a3623df10292a28aeacf9e2c1d2891258479e3b89
+R 06080d39da3db81d6f3cab91a1103d90
+T *branch * wapptest
+T *sym-wapptest *
+T -sym-trunk *
+U dan
+Z 106b952a53453ebb55ee818fa0cbefd7
index fdff836f779ffde79f6e60ef9f2fffe3e1ec1e87..3def84f552a567223abe029b0c6637e78c0af47e 100644 (file)
@@ -1 +1 @@
-80704a16f6dbbeacc65fa36a3623df10292a28aeacf9e2c1d2891258479e3b89
\ No newline at end of file
+a4af0c2fee05aaa2e95ae6a5c847ff2d363e24f325f4ffdcf51bc264b9bf5e2d
\ No newline at end of file
diff --git a/test/releasetest_data.tcl b/test/releasetest_data.tcl
new file mode 100644 (file)
index 0000000..fe08f62
--- /dev/null
@@ -0,0 +1,412 @@
+
+# This file contains Configuration data used by "wapptest.tcl" and
+# "releasetest.tcl".
+#
+
+# Omit comments (text between # and \n) in a long multi-line string.
+#
+proc strip_comments {in} {
+  regsub -all {#[^\n]*\n} $in {} out
+  return $out
+}
+
+array set ::Configs [strip_comments {
+  "Default" {
+    -O2
+    --disable-amalgamation --disable-shared
+    --enable-session
+    -DSQLITE_ENABLE_DESERIALIZE
+  }
+  "Sanitize" {
+    CC=clang -fsanitize=undefined
+    -DSQLITE_ENABLE_STAT4
+    --enable-session
+  }
+  "Stdcall" {
+    -DUSE_STDCALL=1
+    -O2
+  }
+  "Have-Not" {
+    # The "Have-Not" configuration sets all possible -UHAVE_feature options
+    # in order to verify that the code works even on platforms that lack
+    # these support services.
+    -DHAVE_FDATASYNC=0
+    -DHAVE_GMTIME_R=0
+    -DHAVE_ISNAN=0
+    -DHAVE_LOCALTIME_R=0
+    -DHAVE_LOCALTIME_S=0
+    -DHAVE_MALLOC_USABLE_SIZE=0
+    -DHAVE_STRCHRNUL=0
+    -DHAVE_USLEEP=0
+    -DHAVE_UTIME=0
+  }
+  "Unlock-Notify" {
+    -O2
+    -DSQLITE_ENABLE_UNLOCK_NOTIFY
+    -DSQLITE_THREADSAFE
+    -DSQLITE_TCL_DEFAULT_FULLMUTEX=1
+  }
+  "User-Auth" {
+    -O2
+    -DSQLITE_USER_AUTHENTICATION=1
+  }
+  "Secure-Delete" {
+    -O2
+    -DSQLITE_SECURE_DELETE=1
+    -DSQLITE_SOUNDEX=1
+  }
+  "Update-Delete-Limit" {
+    -O2
+    -DSQLITE_DEFAULT_FILE_FORMAT=4
+    -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT=1
+    -DSQLITE_ENABLE_STMT_SCANSTATUS
+    -DSQLITE_LIKE_DOESNT_MATCH_BLOBS
+    -DSQLITE_ENABLE_CURSOR_HINTS
+    --enable-json1
+  }
+  "Check-Symbols" {
+    -DSQLITE_MEMDEBUG=1
+    -DSQLITE_ENABLE_FTS3_PARENTHESIS=1
+    -DSQLITE_ENABLE_FTS3=1
+    -DSQLITE_ENABLE_RTREE=1
+    -DSQLITE_ENABLE_MEMSYS5=1
+    -DSQLITE_ENABLE_MEMSYS3=1
+    -DSQLITE_ENABLE_COLUMN_METADATA=1
+    -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT=1
+    -DSQLITE_SECURE_DELETE=1
+    -DSQLITE_SOUNDEX=1
+    -DSQLITE_ENABLE_ATOMIC_WRITE=1
+    -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1
+    -DSQLITE_ENABLE_OVERSIZE_CELL_CHECK=1
+    -DSQLITE_ENABLE_STAT4
+    -DSQLITE_ENABLE_STMT_SCANSTATUS
+    --enable-json1 --enable-fts5 --enable-session
+  }
+  "Debug-One" {
+    --disable-shared
+    -O2 -funsigned-char
+    -DSQLITE_DEBUG=1
+    -DSQLITE_MEMDEBUG=1
+    -DSQLITE_MUTEX_NOOP=1
+    -DSQLITE_TCL_DEFAULT_FULLMUTEX=1
+    -DSQLITE_ENABLE_FTS3=1
+    -DSQLITE_ENABLE_RTREE=1
+    -DSQLITE_ENABLE_MEMSYS5=1
+    -DSQLITE_ENABLE_COLUMN_METADATA=1
+    -DSQLITE_ENABLE_STAT4
+    -DSQLITE_ENABLE_HIDDEN_COLUMNS
+    -DSQLITE_MAX_ATTACHED=125
+    -DSQLITE_MUTATION_TEST
+    --enable-fts5 --enable-json1
+  }
+  "Fast-One" {
+    -O6
+    -DSQLITE_ENABLE_FTS4=1
+    -DSQLITE_ENABLE_RTREE=1
+    -DSQLITE_ENABLE_STAT4
+    -DSQLITE_ENABLE_RBU
+    -DSQLITE_MAX_ATTACHED=125
+    -DLONGDOUBLE_TYPE=double
+    --enable-session
+  }
+  "Device-One" {
+    -O2
+    -DSQLITE_DEBUG=1
+    -DSQLITE_DEFAULT_AUTOVACUUM=1
+    -DSQLITE_DEFAULT_CACHE_SIZE=64
+    -DSQLITE_DEFAULT_PAGE_SIZE=1024
+    -DSQLITE_DEFAULT_TEMP_CACHE_SIZE=32
+    -DSQLITE_DISABLE_LFS=1
+    -DSQLITE_ENABLE_ATOMIC_WRITE=1
+    -DSQLITE_ENABLE_IOTRACE=1
+    -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1
+    -DSQLITE_MAX_PAGE_SIZE=4096
+    -DSQLITE_OMIT_LOAD_EXTENSION=1
+    -DSQLITE_OMIT_PROGRESS_CALLBACK=1
+    -DSQLITE_OMIT_VIRTUALTABLE=1
+    -DSQLITE_ENABLE_HIDDEN_COLUMNS
+    -DSQLITE_TEMP_STORE=3
+    --enable-json1
+  }
+  "Device-Two" {
+    -DSQLITE_4_BYTE_ALIGNED_MALLOC=1
+    -DSQLITE_DEFAULT_AUTOVACUUM=1
+    -DSQLITE_DEFAULT_CACHE_SIZE=1000
+    -DSQLITE_DEFAULT_LOCKING_MODE=0
+    -DSQLITE_DEFAULT_PAGE_SIZE=1024
+    -DSQLITE_DEFAULT_TEMP_CACHE_SIZE=1000
+    -DSQLITE_DISABLE_LFS=1
+    -DSQLITE_ENABLE_FTS3=1
+    -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1
+    -DSQLITE_ENABLE_RTREE=1
+    -DSQLITE_MAX_COMPOUND_SELECT=50
+    -DSQLITE_MAX_PAGE_SIZE=32768
+    -DSQLITE_OMIT_TRACE=1
+    -DSQLITE_TEMP_STORE=3
+    -DSQLITE_THREADSAFE=2
+    -DSQLITE_ENABLE_DESERIALIZE=1
+    --enable-json1 --enable-fts5 --enable-session
+  }
+  "Locking-Style" {
+    -O2
+    -DSQLITE_ENABLE_LOCKING_STYLE=1
+  }
+  "Apple" {
+    -Os
+    -DHAVE_GMTIME_R=1
+    -DHAVE_ISNAN=1
+    -DHAVE_LOCALTIME_R=1
+    -DHAVE_PREAD=1
+    -DHAVE_PWRITE=1
+    -DHAVE_USLEEP=1
+    -DHAVE_USLEEP=1
+    -DHAVE_UTIME=1
+    -DSQLITE_DEFAULT_CACHE_SIZE=1000
+    -DSQLITE_DEFAULT_CKPTFULLFSYNC=1
+    -DSQLITE_DEFAULT_MEMSTATUS=1
+    -DSQLITE_DEFAULT_PAGE_SIZE=1024
+    -DSQLITE_DISABLE_PAGECACHE_OVERFLOW_STATS=1
+    -DSQLITE_ENABLE_API_ARMOR=1
+    -DSQLITE_ENABLE_AUTO_PROFILE=1
+    -DSQLITE_ENABLE_FLOCKTIMEOUT=1
+    -DSQLITE_ENABLE_FTS3=1
+    -DSQLITE_ENABLE_FTS3_PARENTHESIS=1
+    -DSQLITE_ENABLE_FTS3_TOKENIZER=1
+    if:os=="Darwin" -DSQLITE_ENABLE_LOCKING_STYLE=1
+    -DSQLITE_ENABLE_PERSIST_WAL=1
+    -DSQLITE_ENABLE_PURGEABLE_PCACHE=1
+    -DSQLITE_ENABLE_RTREE=1
+    -DSQLITE_ENABLE_SNAPSHOT=1
+    # -DSQLITE_ENABLE_SQLLOG=1
+    -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT=1
+    -DSQLITE_MAX_LENGTH=2147483645
+    -DSQLITE_MAX_VARIABLE_NUMBER=500000
+    # -DSQLITE_MEMDEBUG=1
+    -DSQLITE_NO_SYNC=1
+    -DSQLITE_OMIT_AUTORESET=1
+    -DSQLITE_OMIT_LOAD_EXTENSION=1
+    -DSQLITE_PREFER_PROXY_LOCKING=1
+    -DSQLITE_SERIES_CONSTRAINT_VERIFY=1
+    -DSQLITE_THREADSAFE=2
+    -DSQLITE_USE_URI=1
+    -DSQLITE_WRITE_WALFRAME_PREBUFFERED=1
+    -DUSE_GUARDED_FD=1
+    -DUSE_PREAD=1
+    --enable-json1 --enable-fts5
+  }
+  "Extra-Robustness" {
+    -DSQLITE_ENABLE_OVERSIZE_CELL_CHECK=1
+    -DSQLITE_MAX_ATTACHED=62
+  }
+  "Devkit" {
+    -DSQLITE_DEFAULT_FILE_FORMAT=4
+    -DSQLITE_MAX_ATTACHED=30
+    -DSQLITE_ENABLE_COLUMN_METADATA
+    -DSQLITE_ENABLE_FTS4
+    -DSQLITE_ENABLE_FTS5
+    -DSQLITE_ENABLE_FTS4_PARENTHESIS
+    -DSQLITE_DISABLE_FTS4_DEFERRED
+    -DSQLITE_ENABLE_RTREE
+    --enable-json1 --enable-fts5
+  }
+  "No-lookaside" {
+    -DSQLITE_TEST_REALLOC_STRESS=1
+    -DSQLITE_OMIT_LOOKASIDE=1
+    -DHAVE_USLEEP=1
+  }
+  "Valgrind" {
+    -DSQLITE_ENABLE_STAT4
+    -DSQLITE_ENABLE_FTS4
+    -DSQLITE_ENABLE_RTREE
+    -DSQLITE_ENABLE_HIDDEN_COLUMNS
+    --enable-json1
+  }
+
+  # The next group of configurations are used only by the
+  # Failure-Detection platform.  They are all the same, but we need
+  # different names for them all so that they results appear in separate
+  # subdirectories.
+  #
+  Fail0 {-O0}
+  Fail2 {-O0}
+  Fail3 {-O0}
+  Fail4 {-O0}
+  FuzzFail1 {-O0}
+  FuzzFail2 {-O0}
+}]
+
+array set ::Platforms [strip_comments {
+  Linux-x86_64 {
+    "Check-Symbols"           checksymbols
+    "Fast-One"                "fuzztest test"
+    "Debug-One"               "mptest test"
+    "Have-Not"                test
+    "Secure-Delete"           test
+    "Unlock-Notify"           "QUICKTEST_INCLUDE=notify2.test test"
+    "User-Auth"               tcltest
+    "Update-Delete-Limit"     test
+    "Extra-Robustness"        test
+    "Device-Two"              test
+    "No-lookaside"            test
+    "Devkit"                  test
+    "Apple"                   test
+    "Sanitize"                {QUICKTEST_OMIT=func4.test,nan.test test}
+    "Device-One"              fulltest
+    "Default"                 "threadtest fulltest"
+    "Valgrind"                valgrindtest
+  }
+  Linux-i686 {
+    "Devkit"                  test
+    "Have-Not"                test
+    "Unlock-Notify"           "QUICKTEST_INCLUDE=notify2.test test"
+    "Device-One"              test
+    "Device-Two"              test
+    "Default"                 "threadtest fulltest"
+  }
+  Darwin-i386 {
+    "Locking-Style"           "mptest test"
+    "Have-Not"                test
+    "Apple"                   "threadtest fulltest"
+  }
+  Darwin-x86_64 {
+    "Locking-Style"           "mptest test"
+    "Have-Not"                test
+    "Apple"                   "threadtest fulltest"
+  }
+  "Windows NT-intel" {
+    "Stdcall"                 test
+    "Have-Not"                test
+    "Default"                 "mptest fulltestonly"
+  }
+  "Windows NT-amd64" {
+    "Stdcall"                 test
+    "Have-Not"                test
+    "Default"                 "mptest fulltestonly"
+  }
+
+  # The Failure-Detection platform runs various tests that deliberately
+  # fail.  This is used as a test of this script to verify that this script
+  # correctly identifies failures.
+  #
+  Failure-Detection {
+    Fail0     "TEST_FAILURE=0 test"
+    Sanitize  "TEST_FAILURE=1 test"
+    Fail2     "TEST_FAILURE=2 valgrindtest"
+    Fail3     "TEST_FAILURE=3 valgrindtest"
+    Fail4     "TEST_FAILURE=4 test"
+    FuzzFail1 "TEST_FAILURE=5 test"
+    FuzzFail2 "TEST_FAILURE=5 valgrindtest"
+  }
+}]
+
+proc make_test_suite {msvc withtcl name testtarget config} {
+
+  # Tcl variable $opts is used to build up the value used to set the
+  # OPTS Makefile variable. Variable $cflags holds the value for
+  # CFLAGS. The makefile will pass OPTS to both gcc and lemon, but
+  # CFLAGS is only passed to gcc.
+  #
+  set makeOpts ""
+  set cflags [expr {$msvc ? "-Zi" : "-g"}]
+  set opts ""
+  set title ${name}($testtarget)
+  set configOpts $withtcl
+  set skip 0
+
+  regsub -all {#[^\n]*\n} $config \n config
+  foreach arg $config {
+    if {$skip} {
+      set skip 0
+      continue
+    }
+    if {[regexp {^-[UD]} $arg]} {
+      lappend opts $arg
+    } elseif {[regexp {^[A-Z]+=} $arg]} {
+      lappend testtarget $arg
+    } elseif {[regexp {^if:([a-z]+)(.*)} $arg all key tail]} {
+      # Arguments of the form 'if:os=="Linux"' will cause the subsequent
+      # argument to be skipped if the $tcl_platform(os) is not "Linux", for
+      # example...
+      set skip [expr !(\$::tcl_platform($key)$tail)]
+    } elseif {[regexp {^--(enable|disable)-} $arg]} {
+      if {$msvc} {
+        if {$arg eq "--disable-amalgamation"} {
+          lappend makeOpts USE_AMALGAMATION=0
+          continue
+        }
+        if {$arg eq "--disable-shared"} {
+          lappend makeOpts USE_CRT_DLL=0 DYNAMIC_SHELL=0
+          continue
+        }
+        if {$arg eq "--enable-fts5"} {
+          lappend opts -DSQLITE_ENABLE_FTS5
+          continue
+        }
+        if {$arg eq "--enable-json1"} {
+          lappend opts -DSQLITE_ENABLE_JSON1
+          continue
+        }
+        if {$arg eq "--enable-shared"} {
+          lappend makeOpts USE_CRT_DLL=1 DYNAMIC_SHELL=1
+          continue
+        }
+      }
+      lappend configOpts $arg
+    } else {
+      if {$msvc} {
+        if {$arg eq "-g"} {
+          lappend cflags -Zi
+          continue
+        }
+        if {[regexp -- {^-O(\d+)$} $arg all level]} then {
+          lappend makeOpts OPTIMIZATIONS=$level
+          continue
+        }
+      }
+      lappend cflags $arg
+    }
+  }
+
+  # Disable sync to make testing faster.
+  #
+  lappend opts -DSQLITE_NO_SYNC=1
+
+  # Some configurations already set HAVE_USLEEP; in that case, skip it.
+  #
+  if {[lsearch -regexp $opts {^-DHAVE_USLEEP(?:=|$)}]==-1} {
+    lappend opts -DHAVE_USLEEP=1
+  }
+
+  # Add the define for this platform.
+  #
+  if {$::tcl_platform(platform)=="windows"} {
+    lappend opts -DSQLITE_OS_WIN=1
+  } else {
+    lappend opts -DSQLITE_OS_UNIX=1
+  }
+
+  # Set the sub-directory to use.
+  #
+  set dir [string tolower [string map {- _ " " _} $name]]
+
+  # Join option lists into strings, using space as delimiter.
+  #
+  set makeOpts [join $makeOpts " "]
+  set cflags   [join $cflags " "]
+  set opts     [join $opts " "]
+
+  return [list $title $dir $configOpts $testtarget $makeOpts $cflags $opts]
+}
+
+# Configuration verification: Check that each entry in the list of configs
+# specified for each platforms exists.
+#
+foreach {key value} [array get ::Platforms] {
+  foreach {v t} $value {
+    if {0==[info exists ::Configs($v)]} {
+      puts stderr "No such configuration: \"$v\""
+      exit -1
+    }
+  }
+}
+
diff --git a/test/wapp.tcl b/test/wapp.tcl
new file mode 100644 (file)
index 0000000..53c21e8
--- /dev/null
@@ -0,0 +1,987 @@
+# Copyright (c) 2017 D. Richard Hipp
+# 
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the Simplified BSD License (also
+# known as the "2-Clause License" or "FreeBSD License".)
+#
+# This program is distributed in the hope that it will be useful,
+# but without any warranty; without even the implied warranty of
+# merchantability or fitness for a particular purpose.
+#
+#---------------------------------------------------------------------------
+#
+# Design rules:
+#
+#   (1)  All identifiers in the global namespace begin with "wapp"
+#
+#   (2)  Indentifiers intended for internal use only begin with "wappInt"
+#
+package require Tcl 8.6
+
+# Add text to the end of the HTTP reply.  No interpretation or transformation
+# of the text is performs.  The argument should be enclosed within {...}
+#
+proc wapp {txt} {
+  global wapp
+  dict append wapp .reply $txt
+}
+
+# Add text to the page under construction.  Do no escaping on the text.
+#
+# Though "unsafe" in general, there are uses for this kind of thing.
+# For example, if you want to return the complete, unmodified content of
+# a file:
+#
+#         set fd [open content.html rb]
+#         wapp-unsafe [read $fd]
+#         close $fd
+#
+# You could do the same thing using ordinary "wapp" instead of "wapp-unsafe".
+# The difference is that wapp-safety-check will complain about the misuse
+# of "wapp", but it assumes that the person who write "wapp-unsafe" understands
+# the risks.
+#
+# Though occasionally necessary, the use of this interface should be minimized.
+#
+proc wapp-unsafe {txt} {
+  global wapp
+  dict append wapp .reply $txt
+}
+
+# Add text to the end of the reply under construction.  The following
+# substitutions are made:
+#
+#     %html(...)          Escape text for inclusion in HTML
+#     %url(...)           Escape text for use as a URL
+#     %qp(...)            Escape text for use as a URI query parameter
+#     %string(...)        Escape text for use within a JSON string
+#     %unsafe(...)        No transformations of the text
+#
+# The substitutions above terminate at the first ")" character.  If the
+# text of the TCL string in ... contains ")" characters itself, use instead:
+#
+#     %html%(...)%
+#     %url%(...)%
+#     %qp%(...)%
+#     %string%(...)%
+#     %unsafe%(...)%
+#
+# In other words, use "%(...)%" instead of "(...)" to include the TCL string
+# to substitute.
+#
+# The %unsafe substitution should be avoided whenever possible, obviously.
+# In addition to the substitutions above, the text also does backslash
+# escapes.
+#
+# The wapp-trim proc works the same as wapp-subst except that it also removes
+# whitespace from the left margin, so that the generated HTML/CSS/Javascript
+# does not appear to be indented when delivered to the client web browser.
+#
+if {$tcl_version>=8.7} {
+  proc wapp-subst {txt} {
+    global wapp
+    regsub -all -command \
+       {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt wappInt-enc txt
+    dict append wapp .reply [subst -novariables -nocommand $txt]
+  }
+  proc wapp-trim {txt} {
+    global wapp
+    regsub -all {\n\s+} [string trim $txt] \n txt
+    regsub -all -command \
+       {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt wappInt-enc txt
+    dict append wapp .reply [subst -novariables -nocommand $txt]
+  }
+  proc wappInt-enc {all mode nu1 txt} {
+    return [uplevel 2 "wappInt-enc-$mode \"$txt\""]
+  }
+} else {
+  proc wapp-subst {txt} {
+    global wapp
+    regsub -all {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt \
+           {[wappInt-enc-\1 "\3"]} txt
+    dict append wapp .reply [uplevel 1 [list subst -novariables $txt]]
+  }
+  proc wapp-trim {txt} {
+    global wapp
+    regsub -all {\n\s+} [string trim $txt] \n txt
+    regsub -all {%(html|url|qp|string|unsafe){1,1}?(|%)\((.+)\)\2} $txt \
+           {[wappInt-enc-\1 "\3"]} txt
+    dict append wapp .reply [uplevel 1 [list subst -novariables $txt]]
+  }
+}
+
+# There must be a wappInt-enc-NAME routine for each possible substitution
+# in wapp-subst.  Thus there are routines for "html", "url", "qp", and "unsafe".
+#
+#    wappInt-enc-html           Escape text so that it is safe to use in the
+#                               body of an HTML document.
+#
+#    wappInt-enc-url            Escape text so that it is safe to pass as an
+#                               argument to href= and src= attributes in HTML.
+#
+#    wappInt-enc-qp             Escape text so that it is safe to use as the
+#                               value of a query parameter in a URL or in
+#                               post data or in a cookie.
+#
+#    wappInt-enc-string         Escape ", ', \, and < for using inside of a
+#                               javascript string literal.  The < character
+#                               is escaped to prevent "</script>" from causing
+#                               problems in embedded javascript.
+#
+#    wappInt-enc-unsafe         Perform no encoding at all.  Unsafe.
+#
+proc wappInt-enc-html {txt} {
+  return [string map {& &amp; < &lt; > &gt; \" &quot; \\ &#92;} $txt]
+}
+proc wappInt-enc-unsafe {txt} {
+  return $txt
+}
+proc wappInt-enc-url {s} {
+  if {[regsub -all {[^-{}@~?=#_.:/a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
+    set s [subst -novar -noback $s]
+  }
+  if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
+    set s [subst -novar -noback $s]
+  }
+  return $s
+}
+proc wappInt-enc-qp {s} {
+  if {[regsub -all {[^-{}_.a-zA-Z0-9]} $s {[wappInt-%HHchar {&}]} s]} {
+    set s [subst -novar -noback $s]
+  }
+  if {[regsub -all {[{}]} $s {[wappInt-%HHchar \\&]} s]} {
+    set s [subst -novar -noback $s]
+  }
+  return $s
+}
+proc wappInt-enc-string {s} {
+  return [string map {\\ \\\\ \" \\\" ' \\' < \\u003c} $s]
+}
+
+# This is a helper routine for wappInt-enc-url and wappInt-enc-qp.  It returns
+# an appropriate %HH encoding for the single character c.  If c is a unicode
+# character, then this routine might return multiple bytes:  %HH%HH%HH
+#
+proc wappInt-%HHchar {c} {
+  if {$c==" "} {return +}
+  return [regsub -all .. [binary encode hex [encoding convertto utf-8 $c]] {%&}]
+}
+
+
+# Undo the www-url-encoded format.
+#
+# HT: This code stolen from ncgi.tcl
+#
+proc wappInt-decode-url {str} {
+  set str [string map [list + { } "\\" "\\\\" \[ \\\[ \] \\\]] $str]
+  regsub -all -- \
+      {%([Ee][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])} \
+      $str {[encoding convertfrom utf-8 [binary decode hex \1\2\3]]} str
+  regsub -all -- \
+      {%([CDcd][A-Fa-f0-9])%([89ABab][A-Fa-f0-9])}                     \
+      $str {[encoding convertfrom utf-8 [binary decode hex \1\2]]} str
+  regsub -all -- {%([0-7][A-Fa-f0-9])} $str {\\u00\1} str
+  return [subst -novar $str]
+}
+
+# Reset the document back to an empty string.
+#
+proc wapp-reset {} {
+  global wapp
+  dict set wapp .reply {}
+}
+
+# Change the mime-type of the result document.
+#
+proc wapp-mimetype {x} {
+  global wapp
+  dict set wapp .mimetype $x
+}
+
+# Change the reply code.
+#
+proc wapp-reply-code {x} {
+  global wapp
+  dict set wapp .reply-code $x
+}
+
+# Set a cookie
+#
+proc wapp-set-cookie {name value} {
+  global wapp
+  dict lappend wapp .new-cookies $name $value
+}
+
+# Unset a cookie
+#
+proc wapp-clear-cookie {name} {
+  wapp-set-cookie $name {}
+}
+
+# Add extra entries to the reply header
+#
+proc wapp-reply-extra {name value} {
+  global wapp
+  dict lappend wapp .reply-extra $name $value
+}
+
+# Specifies how the web-page under construction should be cached.
+# The argument should be one of:
+#
+#    no-cache
+#    max-age=N             (for some integer number of seconds, N)
+#    private,max-age=N
+#
+proc wapp-cache-control {x} {
+  wapp-reply-extra Cache-Control $x
+}
+
+# Redirect to a different web page
+#
+proc wapp-redirect {uri} {
+  wapp-reply-code {307 Redirect}
+  wapp-reply-extra Location $uri
+}
+
+# Return the value of a wapp parameter
+#
+proc wapp-param {name {dflt {}}} {
+  global wapp
+  if {![dict exists $wapp $name]} {return $dflt}
+  return [dict get $wapp $name]
+}
+
+# Return true if a and only if the wapp parameter $name exists
+#
+proc wapp-param-exists {name} {
+  global wapp
+  return [dict exists $wapp $name]
+}
+
+# Set the value of a wapp parameter
+#
+proc wapp-set-param {name value} {
+  global wapp
+  dict set wapp $name $value
+}
+
+# Return all parameter names that match the GLOB pattern, or all
+# names if the GLOB pattern is omitted.
+#
+proc wapp-param-list {{glob {*}}} {
+  global wapp
+  return [dict keys $wapp $glob]
+}
+
+# By default, Wapp does not decode query parameters and POST parameters
+# for cross-origin requests.  This is a security restriction, designed to
+# help prevent cross-site request forgery (CSRF) attacks.
+#
+# As a consequence of this restriction, URLs for sites generated by Wapp
+# that contain query parameters will not work as URLs found in other
+# websites.  You cannot create a link from a second website into a Wapp
+# website if the link contains query planner, by default.
+#
+# Of course, it is sometimes desirable to allow query parameters on external
+# links.  For URLs for which this is safe, the application should invoke
+# wapp-allow-xorigin-params.  This procedure tells Wapp that it is safe to
+# go ahead and decode the query parameters even for cross-site requests.
+#
+# In other words, for Wapp security is the default setting.  Individual pages
+# need to actively disable the cross-site request security if those pages
+# are safe for cross-site access.
+#
+proc wapp-allow-xorigin-params {} {
+  global wapp
+  if {![dict exists $wapp .qp] && ![dict get $wapp SAME_ORIGIN]} {
+    wappInt-decode-query-params
+  }
+}
+
+# Set the content-security-policy.
+#
+# The default content-security-policy is very strict:  "default-src 'self'"
+# The default policy prohibits the use of in-line javascript or CSS.
+#
+# Provide an alternative CSP as the argument.  Or use "off" to disable
+# the CSP completely.
+#
+proc wapp-content-security-policy {val} {
+  global wapp
+  if {$val=="off"} {
+    dict unset wapp .csp
+  } else {
+    dict set wapp .csp $val
+  }
+}
+
+# Examine the bodys of all procedures in this program looking for
+# unsafe calls to various Wapp interfaces.  Return a text string
+# containing warnings. Return an empty string if all is ok.
+#
+# This routine is advisory only.  It misses some constructs that are
+# dangerous and flags others that are safe.
+#
+proc wapp-safety-check {} {
+  set res {}
+  foreach p [info procs] {
+    set ln 0
+    foreach x [split [info body $p] \n] {
+      incr ln
+      if {[regexp {^[ \t]*wapp[ \t]+([^\n]+)} $x all tail]
+       && [string index $tail 0]!="\173"
+       && [regexp {[[$]} $tail]
+      } {
+        append res "$p:$ln: unsafe \"wapp\" call: \"[string trim $x]\"\n"
+      }
+      if {[regexp {^[ \t]*wapp-(subst|trim)[ \t]+[^\173]} $x all cx]} {
+        append res "$p:$ln: unsafe \"wapp-$cx\" call: \"[string trim $x]\"\n"
+      }
+    }
+  }
+  return $res
+}
+
+# Return a string that descripts the current environment.  Applications
+# might find this useful for debugging.
+#
+proc wapp-debug-env {} {
+  global wapp
+  set out {}
+  foreach var [lsort [dict keys $wapp]] {
+    if {[string index $var 0]=="."} continue
+    append out "$var = [list [dict get $wapp $var]]\n"
+  }
+  append out "\[pwd\] = [list [pwd]]\n"
+  return $out
+}
+
+# Tracing function for each HTTP request.  This is overridden by wapp-start
+# if tracing is enabled.
+#
+proc wappInt-trace {} {}
+
+# Start up a listening socket.  Arrange to invoke wappInt-new-connection
+# for each inbound HTTP connection.
+#
+#    port            Listen on this TCP port.  0 means to select a port
+#                    that is not currently in use
+#
+#    wappmode        One of "scgi", "remote-scgi", "server", or "local".
+#
+#    fromip          If not {}, then reject all requests from IP addresses
+#                    other than $fromip
+#
+proc wappInt-start-listener {port wappmode fromip} {
+  if {[string match *scgi $wappmode]} {
+    set type SCGI
+    set server [list wappInt-new-connection \
+                wappInt-scgi-readable $wappmode $fromip]
+  } else {
+    set type HTTP
+    set server [list wappInt-new-connection \
+                wappInt-http-readable $wappmode $fromip]
+  }
+  if {$wappmode=="local" || $wappmode=="scgi"} {
+    set x [socket -server $server -myaddr 127.0.0.1 $port]
+  } else {
+    set x [socket -server $server $port]
+  }
+  set coninfo [chan configure $x -sockname]
+  set port [lindex $coninfo 2]
+  if {$wappmode=="local"} {
+    wappInt-start-browser http://127.0.0.1:$port/
+  } elseif {$fromip!=""} {
+    puts "Listening for $type requests on TCP port $port from IP $fromip"
+  } else {
+    puts "Listening for $type requests on TCP port $port"
+  }
+}
+
+# Start a web-browser and point it at $URL
+#
+proc wappInt-start-browser {url} {
+  global tcl_platform
+  if {$tcl_platform(platform)=="windows"} {
+    exec cmd /c start $url &
+  } elseif {$tcl_platform(os)=="Darwin"} {
+    exec open $url &
+  } elseif {[catch {exec xdg-open $url}]} {
+    exec firefox $url &
+  }
+}
+
+# This routine is a "socket -server" callback.  The $chan, $ip, and $port
+# arguments are added by the socket command.
+#
+# Arrange to invoke $callback when content is available on the new socket.
+# The $callback will process inbound HTTP or SCGI content.  Reject the
+# request if $fromip is not an empty string and does not match $ip.
+#
+proc wappInt-new-connection {callback wappmode fromip chan ip port} {
+  upvar #0 wappInt-$chan W
+  if {$fromip!="" && ![string match $fromip $ip]} {
+    close $chan
+    return
+  }
+  set W [dict create REMOTE_ADDR $ip REMOTE_PORT $port WAPP_MODE $wappmode \
+         .header {}]
+  fconfigure $chan -blocking 0 -translation binary
+  fileevent $chan readable [list $callback $chan]
+}
+
+# Close an input channel
+#
+proc wappInt-close-channel {chan} {
+  if {$chan=="stdout"} {
+    # This happens after completing a CGI request
+    exit 0
+  } else {
+    unset ::wappInt-$chan
+    close $chan
+  }
+}
+
+# Process new text received on an inbound HTTP request
+#
+proc wappInt-http-readable {chan} {
+  if {[catch [list wappInt-http-readable-unsafe $chan] msg]} {
+    puts stderr "$msg\n$::errorInfo"
+    wappInt-close-channel $chan
+  }
+}
+proc wappInt-http-readable-unsafe {chan} {
+  upvar #0 wappInt-$chan W wapp wapp
+  if {![dict exists $W .toread]} {
+    # If the .toread key is not set, that means we are still reading
+    # the header
+    set line [string trimright [gets $chan]]
+    set n [string length $line]
+    if {$n>0} {
+      if {[dict get $W .header]=="" || [regexp {^\s+} $line]} {
+        dict append W .header $line
+      } else {
+        dict append W .header \n$line
+      }
+      if {[string length [dict get $W .header]]>100000} {
+        error "HTTP request header too big - possible DOS attack"
+      }
+    } elseif {$n==0} {
+      # We have reached the blank line that terminates the header.
+      global argv0
+      set a0 [file normalize $argv0]
+      dict set W SCRIPT_FILENAME $a0
+      dict set W DOCUMENT_ROOT [file dir $a0]
+      if {[wappInt-parse-header $chan]} {
+        catch {close $chan}
+        return
+      }
+      set len 0
+      if {[dict exists $W CONTENT_LENGTH]} {
+        set len [dict get $W CONTENT_LENGTH]
+      }
+      if {$len>0} {
+        # Still need to read the query content
+        dict set W .toread $len
+      } else {
+        # There is no query content, so handle the request immediately
+        set wapp $W
+        wappInt-handle-request $chan 0
+      }
+    }
+  } else {
+    # If .toread is set, that means we are reading the query content.
+    # Continue reading until .toread reaches zero.
+    set got [read $chan [dict get $W .toread]]
+    dict append W CONTENT $got
+    dict set W .toread [expr {[dict get $W .toread]-[string length $got]}]
+    if {[dict get $W .toread]<=0} {
+      # Handle the request as soon as all the query content is received
+      set wapp $W
+      wappInt-handle-request $chan 0
+    }
+  }
+}
+
+# Decode the HTTP request header.
+#
+# This routine is always running inside of a [catch], so if
+# any problems arise, simply raise an error.
+#
+proc wappInt-parse-header {chan} {
+  upvar #0 wappInt-$chan W
+  set hdr [split [dict get $W .header] \n]
+  if {$hdr==""} {return 1}
+  set req [lindex $hdr 0]
+  dict set W REQUEST_METHOD [set method [lindex $req 0]]
+  if {[lsearch {GET HEAD POST} $method]<0} {
+    error "unsupported request method: \"[dict get $W REQUEST_METHOD]\""
+  }
+  set uri [lindex $req 1]
+  set split_uri [split $uri ?]
+  set uri0 [lindex $split_uri 0]
+  if {![regexp {^/[-.a-z0-9_/]*$} $uri0]} {
+    error "invalid request uri: \"$uri0\""
+  }
+  dict set W REQUEST_URI $uri0
+  dict set W PATH_INFO $uri0
+  set uri1 [lindex $split_uri 1]
+  dict set W QUERY_STRING $uri1
+  set n [llength $hdr]
+  for {set i 1} {$i<$n} {incr i} {
+    set x [lindex $hdr $i]
+    if {![regexp {^(.+): +(.*)$} $x all name value]} {
+      error "invalid header line: \"$x\""
+    }
+    set name [string toupper $name]
+    switch -- $name {
+      REFERER {set name HTTP_REFERER}
+      USER-AGENT {set name HTTP_USER_AGENT}
+      CONTENT-LENGTH {set name CONTENT_LENGTH}
+      CONTENT-TYPE {set name CONTENT_TYPE}
+      HOST {set name HTTP_HOST}
+      COOKIE {set name HTTP_COOKIE}
+      ACCEPT-ENCODING {set name HTTP_ACCEPT_ENCODING}
+      default {set name .hdr:$name}
+    }
+    dict set W $name $value
+  }
+  return 0
+}
+
+# Decode the QUERY_STRING parameters from a GET request or the
+# application/x-www-form-urlencoded CONTENT from a POST request.
+#
+# This routine sets the ".qp" element of the ::wapp dict as a signal
+# that query parameters have already been decoded.
+#
+proc wappInt-decode-query-params {} {
+  global wapp
+  dict set wapp .qp 1
+  if {[dict exists $wapp QUERY_STRING]} {
+    foreach qterm [split [dict get $wapp QUERY_STRING] &] {
+      set qsplit [split $qterm =]
+      set nm [lindex $qsplit 0]
+      if {[regexp {^[a-z][a-z0-9]*$} $nm]} {
+        dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
+      }
+    }
+  }
+  if {[dict exists $wapp CONTENT_TYPE] && [dict exists $wapp CONTENT]} {
+    set ctype [dict get $wapp CONTENT_TYPE]
+    if {$ctype=="application/x-www-form-urlencoded"} {
+      foreach qterm [split [string trim [dict get $wapp CONTENT]] &] {
+        set qsplit [split $qterm =]
+        set nm [lindex $qsplit 0]
+        if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
+          dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
+        }
+      }
+    } elseif {[string match multipart/form-data* $ctype]} {
+      regexp {^(.*?)\r\n(.*)$} [dict get $wapp CONTENT] all divider body
+      set ndiv [string length $divider]
+      while {[string length $body]} {
+        set idx [string first $divider $body]
+        set unit [string range $body 0 [expr {$idx-3}]]
+        set body [string range $body [expr {$idx+$ndiv+2}] end]
+        if {[regexp {^Content-Disposition: form-data; (.*?)\r\n\r\n(.*)$} \
+             $unit unit hdr content]} {
+          if {[regexp {name="(.*)"; filename="(.*)"\r\nContent-Type: (.*?)$}\
+                $hdr hr name filename mimetype]} {
+            dict set wapp $name.filename \
+              [string map [list \\\" \" \\\\ \\] $filename]
+            dict set wapp $name.mimetype $mimetype
+            dict set wapp $name.content $content
+          } elseif {[regexp {name="(.*)"} $hdr hr name]} {
+            dict set wapp $name $content
+          }
+        }
+      }
+    }
+  }
+}
+
+# Invoke application-supplied methods to generate a reply to
+# a single HTTP request.
+#
+# This routine always runs within [catch], so handle exceptions by
+# invoking [error].
+#
+proc wappInt-handle-request {chan useCgi} {
+  global wapp
+  dict set wapp .reply {}
+  dict set wapp .mimetype {text/html; charset=utf-8}
+  dict set wapp .reply-code {200 Ok}
+  dict set wapp .csp {default-src 'self'}
+
+  # Set up additional CGI environment values
+  #
+  if {![dict exists $wapp HTTP_HOST]} {
+    dict set wapp BASE_URL {}
+  } elseif {[dict exists $wapp HTTPS]} {
+    dict set wapp BASE_URL https://[dict get $wapp HTTP_HOST]
+  } else {
+    dict set wapp BASE_URL http://[dict get $wapp HTTP_HOST]
+  }
+  if {![dict exists $wapp REQUEST_URI]} {
+    dict set wapp REQUEST_URI /
+  } elseif {[regsub {\?.*} [dict get $wapp REQUEST_URI] {} newR]} {
+    # Some servers (ex: nginx) append the query parameters to REQUEST_URI.
+    # These need to be stripped off
+    dict set wapp REQUEST_URI $newR
+  }
+  if {[dict exists $wapp SCRIPT_NAME]} {
+    dict append wapp BASE_URL [dict get $wapp SCRIPT_NAME]
+  } else {
+    dict set wapp SCRIPT_NAME {}
+  }
+  if {![dict exists $wapp PATH_INFO]} {
+    # If PATH_INFO is missing (ex: nginx) then construct it
+    set URI [dict get $wapp REQUEST_URI]
+    set skip [string length [dict get $wapp SCRIPT_NAME]]
+    dict set wapp PATH_INFO [string range $URI $skip end]
+  }
+  if {[regexp {^/([^/]+)(.*)$} [dict get $wapp PATH_INFO] all head tail]} {
+    dict set wapp PATH_HEAD $head
+    dict set wapp PATH_TAIL [string trimleft $tail /]
+  } else {
+    dict set wapp PATH_INFO {}
+    dict set wapp PATH_HEAD {}
+    dict set wapp PATH_TAIL {}
+  }
+  dict set wapp SELF_URL [dict get $wapp BASE_URL]/[dict get $wapp PATH_HEAD]
+
+  # Parse query parameters from the query string, the cookies, and
+  # POST data
+  #
+  if {[dict exists $wapp HTTP_COOKIE]} {
+    foreach qterm [split [dict get $wapp HTTP_COOKIE] {;}] {
+      set qsplit [split [string trim $qterm] =]
+      set nm [lindex $qsplit 0]
+      if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
+        dict set wapp $nm [wappInt-decode-url [lindex $qsplit 1]]
+      }
+    }
+  }
+  set same_origin 0
+  if {[dict exists $wapp HTTP_REFERER]} {
+    set referer [dict get $wapp HTTP_REFERER]
+    set base [dict get $wapp BASE_URL]
+    if {$referer==$base || [string match $base/* $referer]} {
+      set same_origin 1
+    }
+  }
+  dict set wapp SAME_ORIGIN $same_origin
+  if {$same_origin} {
+    wappInt-decode-query-params
+  }
+
+  # Invoke the application-defined handler procedure for this page
+  # request.  If an error occurs while running that procedure, generate
+  # an HTTP reply that contains the error message.
+  #
+  wapp-before-dispatch-hook
+  wappInt-trace
+  set mname [dict get $wapp PATH_HEAD]
+  if {[catch {
+    if {$mname!="" && [llength [info proc wapp-page-$mname]]>0} {
+      wapp-page-$mname
+    } else {
+      wapp-default
+    }
+  } msg]} {
+    if {[wapp-param WAPP_MODE]=="local" || [wapp-param WAPP_MODE]=="server"} {
+      puts "ERROR: $::errorInfo"
+    }
+    wapp-reset
+    wapp-reply-code "500 Internal Server Error"
+    wapp-mimetype text/html
+    wapp-trim {
+      <h1>Wapp Application Error</h1>
+      <pre>%html($::errorInfo)</pre>
+    }
+    dict unset wapp .new-cookies
+  }
+
+  # Transmit the HTTP reply
+  #
+  if {$chan=="stdout"} {
+    puts $chan "Status: [dict get $wapp .reply-code]\r"
+  } else {
+    puts $chan "HTTP/1.1 [dict get $wapp .reply-code]\r"
+    puts $chan "Server: wapp\r"
+    puts $chan "Connection: close\r"
+  }
+  if {[dict exists $wapp .reply-extra]} {
+    foreach {name value} [dict get $wapp .reply-extra] {
+      puts $chan "$name: $value\r"
+    }
+  }
+  if {[dict exists $wapp .csp]} {
+    puts $chan "Content-Security-Policy: [dict get $wapp .csp]\r"
+  }
+  set mimetype [dict get $wapp .mimetype]
+  puts $chan "Content-Type: $mimetype\r"
+  if {[dict exists $wapp .new-cookies]} {
+    foreach {nm val} [dict get $wapp .new-cookies] {
+      if {[regexp {^[a-z][-a-z0-9_]*$} $nm]} {
+        if {$val==""} {
+          puts $chan "Set-Cookie: $nm=; HttpOnly; Path=/; Max-Age=1\r"
+        } else {
+          set val [wappInt-enc-url $val]
+          puts $chan "Set-Cookie: $nm=$val; HttpOnly; Path=/\r"
+        }
+      }
+    }
+  }
+  if {[string match text/* $mimetype]} {
+    set reply [encoding convertto utf-8 [dict get $wapp .reply]]
+    if {[regexp {\ygzip\y} [wapp-param HTTP_ACCEPT_ENCODING]]} {
+      catch {
+        set x [zlib gzip $reply]
+        set reply $x
+        puts $chan "Content-Encoding: gzip\r"
+      }
+    }
+  } else {
+    set reply [dict get $wapp .reply]
+  }
+  puts $chan "Content-Length: [string length $reply]\r"
+  puts $chan \r
+  puts -nonewline $chan $reply
+  flush $chan
+  wappInt-close-channel $chan
+}
+
+# This routine runs just prior to request-handler dispatch.  The
+# default implementation is a no-op, but applications can override
+# to do additional transformations or checks.
+#
+proc wapp-before-dispatch-hook {} {return}
+
+# Process a single CGI request
+#
+proc wappInt-handle-cgi-request {} {
+  global wapp env
+  foreach key {
+    CONTENT_LENGTH
+    CONTENT_TYPE
+    DOCUMENT_ROOT
+    HTTP_ACCEPT_ENCODING
+    HTTP_COOKIE
+    HTTP_HOST
+    HTTP_REFERER
+    HTTP_USER_AGENT
+    HTTPS
+    PATH_INFO
+    QUERY_STRING
+    REMOTE_ADDR
+    REQUEST_METHOD
+    REQUEST_URI
+    REMOTE_USER
+    SCRIPT_FILENAME
+    SCRIPT_NAME
+    SERVER_NAME
+    SERVER_PORT
+    SERVER_PROTOCOL
+  } {
+    if {[info exists env($key)]} {
+      dict set wapp $key $env($key)
+    }
+  }
+  set len 0
+  if {[dict exists $wapp CONTENT_LENGTH]} {
+    set len [dict get $wapp CONTENT_LENGTH]
+  }
+  if {$len>0} {
+    fconfigure stdin -translation binary
+    dict set wapp CONTENT [read stdin $len]
+  }
+  dict set wapp WAPP_MODE cgi
+  fconfigure stdout -translation binary
+  wappInt-handle-request stdout 1
+}
+
+# Process new text received on an inbound SCGI request
+#
+proc wappInt-scgi-readable {chan} {
+  if {[catch [list wappInt-scgi-readable-unsafe $chan] msg]} {
+    puts stderr "$msg\n$::errorInfo"
+    wappInt-close-channel $chan
+  }
+}
+proc wappInt-scgi-readable-unsafe {chan} {
+  upvar #0 wappInt-$chan W wapp wapp
+  if {![dict exists $W .toread]} {
+    # If the .toread key is not set, that means we are still reading
+    # the header.
+    #
+    # An SGI header is short.  This implementation assumes the entire
+    # header is available all at once.
+    #
+    dict set W .remove_addr [dict get $W REMOTE_ADDR]
+    set req [read $chan 15]
+    set n [string length $req]
+    scan $req %d:%s len hdr
+    incr len [string length "$len:,"]
+    append hdr [read $chan [expr {$len-15}]]
+    foreach {nm val} [split $hdr \000] {
+      if {$nm==","} break
+      dict set W $nm $val
+    }
+    set len 0
+    if {[dict exists $W CONTENT_LENGTH]} {
+      set len [dict get $W CONTENT_LENGTH]
+    }
+    if {$len>0} {
+      # Still need to read the query content
+      dict set W .toread $len
+    } else {
+      # There is no query content, so handle the request immediately
+      dict set W SERVER_ADDR [dict get $W .remove_addr]
+      set wapp $W
+      wappInt-handle-request $chan 0
+    }
+  } else {
+    # If .toread is set, that means we are reading the query content.
+    # Continue reading until .toread reaches zero.
+    set got [read $chan [dict get $W .toread]]
+    dict append W CONTENT $got
+    dict set W .toread [expr {[dict get $W .toread]-[string length $got]}]
+    if {[dict get $W .toread]<=0} {
+      # Handle the request as soon as all the query content is received
+      dict set W SERVER_ADDR [dict get $W .remove_addr]
+      set wapp $W
+      wappInt-handle-request $chan 0
+    }
+  }
+}
+
+# Start up the wapp framework.  Parameters are a list passed as the
+# single argument.
+#
+#    -server $PORT         Listen for HTTP requests on this TCP port $PORT
+#
+#    -local $PORT          Listen for HTTP requests on 127.0.0.1:$PORT
+#
+#    -scgi $PORT           Listen for SCGI requests on 127.0.0.1:$PORT
+#
+#    -remote-scgi $PORT    Listen for SCGI requests on TCP port $PORT
+#
+#    -cgi                  Handle a single CGI request
+#
+# With no arguments, the behavior is called "auto".  In "auto" mode,
+# if the GATEWAY_INTERFACE environment variable indicates CGI, then run
+# as CGI.  Otherwise, start an HTTP server bound to the loopback address
+# only, on an arbitrary TCP port, and automatically launch a web browser
+# on that TCP port.
+#
+# Additional options:
+#
+#    -fromip GLOB         Reject any incoming request where the remote
+#                         IP address does not match the GLOB pattern.  This
+#                         value defaults to '127.0.0.1' for -local and -scgi.
+#
+#    -nowait              Do not wait in the event loop.  Return immediately
+#                         after all event handlers are established.
+#
+#    -trace               "puts" each request URL as it is handled, for
+#                         debugging
+#
+#    -lint                Run wapp-safety-check on the application instead
+#                         of running the application itself
+#
+#    -Dvar=value          Set TCL global variable "var" to "value"
+#
+#
+proc wapp-start {arglist} {
+  global env
+  set mode auto
+  set port 0
+  set nowait 0
+  set fromip {}
+  set n [llength $arglist]
+  for {set i 0} {$i<$n} {incr i} {
+    set term [lindex $arglist $i]
+    if {[string match --* $term]} {set term [string range $term 1 end]}
+    switch -glob -- $term {
+      -server {
+        incr i;
+        set mode "server"
+        set port [lindex $arglist $i]
+      }
+      -local {
+        incr i;
+        set mode "local"
+        set fromip 127.0.0.1
+        set port [lindex $arglist $i]
+      }
+      -scgi {
+        incr i;
+        set mode "scgi"
+        set fromip 127.0.0.1
+        set port [lindex $arglist $i]
+      }
+      -remote-scgi {
+        incr i;
+        set mode "remote-scgi"
+        set port [lindex $arglist $i]
+      }
+      -cgi {
+        set mode "cgi"
+      }
+      -fromip {
+        incr i
+        set fromip [lindex $arglist $i]
+      }
+      -nowait {
+        set nowait 1
+      }
+      -trace {
+        proc wappInt-trace {} {
+          set q [wapp-param QUERY_STRING]
+          set uri [wapp-param BASE_URL][wapp-param PATH_INFO]
+          if {$q!=""} {append uri ?$q}
+          puts $uri
+        }
+      }
+      -lint {
+        set res [wapp-safety-check]
+        if {$res!=""} {
+          puts "Potential problems in this code:"
+          puts $res
+          exit 1
+        } else {
+          exit
+        }
+      }
+      -D*=* {
+        if {[regexp {^.D([^=]+)=(.*)$} $term all var val]} {
+          set ::$var $val
+        }
+      }
+      default {
+        error "unknown option: $term"
+      }
+    }
+  }
+  if {$mode=="auto"} {
+    if {[info exists env(GATEWAY_INTERFACE)]
+        && [string match CGI/1.* $env(GATEWAY_INTERFACE)]} {
+      set mode cgi
+    } else {
+      set mode local
+    }
+  }
+  if {$mode=="cgi"} {
+    wappInt-handle-cgi-request
+  } else {
+    wappInt-start-listener $port $mode $fromip
+    if {!$nowait} {
+      vwait ::forever
+    }
+  }
+}
+
+# Call this version 1.0
+package provide wapp 1.0
diff --git a/test/wapptest.tcl b/test/wapptest.tcl
new file mode 100755 (executable)
index 0000000..cb9234b
--- /dev/null
@@ -0,0 +1,506 @@
+#!/bin/sh 
+# \
+exec wapptclsh "$0" ${1+"$@"}
+
+#
+#
+#
+
+# Variables set by the "control" form:
+#
+#   G(platform) - User selected platform.
+#   G(test)     - Set to "Normal", "Veryquick", "Smoketest" or "Build-Only".
+#   G(keep)     - Boolean. True to delete no files after each test.
+#   G(msvc)     - Boolean. True to use MSVC as the compiler.
+#   G(tcl)      - Use Tcl from this directory for builds.
+#   G(jobs)     - How many sub-processes to run simultaneously.
+#
+set G(platform) $::tcl_platform(os)-$::tcl_platform(machine)
+set G(test)     Normal
+set G(keep)     0
+set G(msvc)     0
+set G(tcl)      ""
+set G(jobs)     3
+
+set G(sqlite_version) unknown
+
+# The root of the SQLite source tree.
+#
+set G(srcdir)   [file dirname [file dirname [info script]]]
+
+# Either "config", "running", "stopped":
+#
+set G(state) "config"
+
+# releasetest.tcl script
+#
+set G(releaseTest) [file join [file dirname [info script]] releasetest.tcl]
+
+set G(cnt) 0
+
+# package required wapp
+source [file join [file dirname [info script]] wapp.tcl]
+
+# Read the data from the releasetest_data.tcl script.
+#
+source [file join [file dirname [info script]] releasetest_data.tcl]
+
+# Check to see if there are uncommitted changes in the SQLite source
+# directory. Return true if there are, or false otherwise.
+#
+proc check_uncommitted {} {
+  global G
+  set ret 0
+  set pwd [pwd]
+  cd $G(srcdir)
+  if {[catch {exec fossil changes} res]==0 && [string trim $res]!=""} {
+    set ret 1
+  }
+  cd $pwd
+  return $ret
+}
+
+# If the application is in "config" state, set the contents of the 
+# ::G(test_array) global to reflect the tests that will be run. If the
+# app is in some other state ("running" or "stopped"), this command
+# is a no-op.
+#
+proc set_test_array {} {
+  global G
+  if { $G(state)=="config" } {
+    set G(test_array) [list]
+    foreach {config target} $::Platforms($G(platform)) {
+
+      # If using MSVC, do not run sanitize or valgrind tests. Or the
+      # checksymbols test.
+      if {$G(msvc) && (
+          "Sanitize" == $config 
+       || "checksymbols" in $target
+       || "valgrindtest" in $target
+      )} {
+        continue
+      }
+
+      # If the test mode is not "Normal", override the target.
+      #
+      if {$target!="checksymbols" && $G(platform)!="Failure-Detection"} {
+        switch -- $G(test) {
+          Veryquick { set target quicktest }
+          Smoketest { set target smoketest }
+          Build-Only {
+            set target testfixture
+            if {$::tcl_platform(platform)=="windows"} {
+              set target testfixture.exe
+            }
+          }
+        }
+      }
+
+      lappend G(test_array) [dict create config $config target $target]
+    }
+  }
+}
+
+proc count_tests_and_errors {name logfile} {
+  global G
+
+  set fd [open $logfile rb]
+  set seen 0
+  while {![eof $fd]} {
+    set line [gets $fd]
+    if {[regexp {(\d+) errors out of (\d+) tests} $line all nerr ntest]} {
+      incr G(test.$name.nError) $nerr
+      incr G(test.$name.nTest) $ntest
+      set seen 1
+      if {$nerr>0} {
+        set G(test.$name.errmsg) $line
+      }
+    }
+    if {[regexp {runtime error: +(.*)} $line all msg]} {
+      # skip over "value is outside range" errors
+      if {[regexp {value .* is outside the range of representable} $line]} {
+         # noop
+      } else {
+        incr G(test.$name.nError)
+        if {$G(test.$name.errmsg)==""} {
+          set G(test.$name.errmsg) $msg
+        }
+      }
+    }
+    if {[regexp {fatal error +(.*)} $line all msg]} {
+      incr G(test.$name.nError)
+      if {$G(test.$name.errmsg)==""} {
+        set G(test.$name.errmsg) $msg
+      }
+    }
+    if {[regexp {ERROR SUMMARY: (\d+) errors.*} $line all cnt] && $cnt>0} {
+      incr G(test.$name.nError)
+      if {$G(test.$name.errmsg)==""} {
+        set G(test.$name.errmsg) $all
+      }
+    }
+    if {[regexp {^VERSION: 3\.\d+.\d+} $line]} {
+      set v [string range $line 9 end]
+      if {$G(sqlite_version) eq "unknown"} {
+        set G(sqlite_version) $v
+      } elseif {$G(sqlite_version) ne $v} {
+        set G(test.$name.errmsg) "version conflict: {$G(sqlite_version)} vs. {$v}"
+      }
+    }
+  }
+  close $fd
+  if {$G(test) == "Build-Only"} {
+    incr G(test.$name.nTest)
+    if {$G(test.$name.nError)>0} {
+      set errmsg "Build failed"
+    }
+  } elseif {!$seen} {
+    set G(test.$name.errmsg) "Test did not complete"
+    if {[file readable core]} {
+      append G(test.$name.errmsg) " - core file exists"
+    }
+  }
+}
+
+proc slave_fileevent {name} {
+  global G
+  set fd $G(test.$name.channel)
+
+  if {[eof $fd]} {
+    fconfigure $fd -blocking 1
+    set rc [catch { close $fd }]
+    unset G(test.$name.channel)
+    set G(test.$name.done) [clock seconds]
+    set G(test.$name.nError) 0
+    set G(test.$name.nTest) 0
+    set G(test.$name.errmsg) ""
+    if {$rc} {
+      incr G(test.$name.nError)
+    }
+    if {[file exists $G(test.$name.log)]} {
+      count_tests_and_errors $name $G(test.$name.log)
+    }
+  } else {
+    set line [gets $fd]
+    if {[string trim $line] != ""} { puts "Trace   : $name - \"$line\"" }
+  }
+
+  do_some_stuff
+}
+
+proc do_some_stuff {} {
+  global G
+
+  # Count the number of running jobs. A running job has an entry named
+  # "channel" in its dictionary.
+  set nRunning 0
+  set bFinished 1
+  foreach j $G(test_array) {
+    set name [dict get $j config]
+    if { [info exists G(test.$name.channel)]} { incr nRunning   }
+    if {![info exists G(test.$name.done)]}    { set bFinished 0 }
+  }
+
+  if {$bFinished} {
+    set nError 0
+    set nTest 0
+    set nConfig 0
+    foreach j $G(test_array) {
+      set name [dict get $j config]
+      incr nError $G(test.$name.nError)
+      incr nTest $G(test.$name.nTest)
+      incr nConfig 
+    }
+    set G(result) "$nError errors from $nTest tests in $nConfig configurations."
+    catch {
+      append G(result) " SQLite version $G(sqlite_version)"
+    }
+  } else {
+    set nLaunch [expr $G(jobs) - $nRunning]
+    foreach j $G(test_array) {
+      if {$nLaunch<=0} break
+      set name [dict get $j config]
+      if { ![info exists G(test.$name.channel)]
+        && ![info exists G(test.$name.done)]
+      } {
+        set target [dict get $j target]
+        set G(test.$name.start) [clock seconds]
+        set fd [open "|[info nameofexecutable] $G(releaseTest) --slave" r+]
+        set G(test.$name.channel) $fd
+        fconfigure $fd -blocking 0
+        fileevent $fd readable [list slave_fileevent $name]
+
+        puts $fd [list 0 $G(msvc) 0 $G(keep)]
+        set L [make_test_suite $G(msvc) "" $name $target $::Configs($name)]
+        puts $fd $L
+        flush $fd
+        set G(test.$name.log) [file join [lindex $L 1] test.log]
+        incr nLaunch -1
+      }
+    }
+  }
+}
+
+proc generate_main_page {{extra {}}} {
+  global G
+  set_test_array
+
+  wapp-trim {
+    <html>
+    <head>
+      <link rel="stylesheet" type="text/css" href="style.css"/>
+    </head>
+    <body>
+  }
+
+  # If the checkout contains uncommitted changs, put a warning at the top
+  # of the page.
+  if {[check_uncommitted]} {
+    wapp-trim {
+      <div class=warning>
+        WARNING: Uncommitted changes in checkout.
+      </div>
+    }
+  }
+
+  wapp-trim {
+      <div class=div id=controls> 
+        <form action="control" method="post" name="control">
+        <label> Platform: </label>
+        <select id="control_platform" name="control_platform">
+  }
+  foreach platform [array names ::Platforms] {
+    set selected ""
+    if {$platform==$G(platform)} { set selected " selected=1" }
+    wapp-subst "<option $selected>$platform</option>"
+  }
+  wapp-trim {
+        </select>
+        <label> Test: </label>
+        <select id="control_test" name="control_test">
+  }
+  foreach test [list Normal Veryquick Smoketest Build-Only] {
+    set selected ""
+    if {$test==$G(test)} { set selected " selected=1" }
+    wapp-subst "<option $selected>$test</option>"
+  }
+  wapp-trim [subst -nocommands {
+        </select>
+        <label> Tcl: </label>
+        <input id="control_tcl" name="control_tcl"></input>
+
+        <label> Keep files: </label>
+        <input id="control_keep" name="control_keep" type=checkbox value=1>
+        </input>
+        <label> Use MSVC: </label>
+        <input id="control_msvc" name="control_msvc" type=checkbox value=1>
+        </input>
+        <hr>
+        <div class=right>
+          <label> Jobs: </label>
+          <select id="control_jobs" name="control_jobs">
+  }]
+  for {set i 1} {$i <= 8} {incr i} {
+    if {$G(jobs)==$i} {
+      wapp-trim {
+        <option selected=1>%string($i)</option>
+      }
+    } else {
+      wapp-trim {
+        <option>%string($i)</option>
+      }
+    }
+  }
+  wapp-trim {
+          </select>
+          <input id=control_go name=control_go type=submit value="Run Tests!">
+          </input>
+        </div>
+     </form>
+      </div>
+      <div class=div id=tests>    
+      <table>
+  }
+  foreach t $G(test_array) {
+    set config [dict get $t config]
+    set target [dict get $t target]
+
+    set class "testwait"
+    set seconds ""
+
+    if {[info exists G(test.$config.log)]} {
+      if {[info exists G(test.$config.channel)]} {
+        set class "testrunning"
+        set seconds [expr [clock seconds] - $G(test.$config.start)]
+      } elseif {[info exists G(test.$config.done)]} {
+        if {$G(test.$config.nError)>0} {
+          set class "testfail" 
+        } else {
+          set class "testdone"
+        }
+        set seconds [expr $G(test.$config.done) - $G(test.$config.start)]
+      }
+
+      set min [format %.2d [expr ($seconds / 60) % 60]]
+      set  hr [format %.2d [expr $seconds / 3600]]
+      set sec [format %.2d [expr $seconds % 60]]
+      set seconds "$hr:$min:$sec"
+    }
+
+    wapp-trim {
+      <tr class=%string($class)>
+      <td class=testfield> %html($config) 
+      <td class=testfield> %html($target)
+      <td class=testfield> %html($seconds)
+      <td class=testfield>
+    }
+    if {[info exists G(test.$config.log)]} {
+      set log $G(test.$config.log)
+      set uri "log/$log"
+      wapp-trim {
+        <a href=%url($uri)> %html($log) </a>
+      }
+    }
+    if {[info exists G(test.$config.errmsg)] && $G(test.$config.errmsg)!=""} {
+      set errmsg $G(test.$config.errmsg)
+      wapp-trim {
+        <tr class=testfail>
+        <td class=testfield>
+        <td class=testfield colspan=3> %html($errmsg)
+      }
+    }
+  }
+
+  wapp-trim {
+      </table>
+      </div>
+  }
+  if {[info exists G(result)]} {
+    set res $G(result)
+    wapp-trim {
+      <div class=div id=log> %string($res) </div>
+    }
+  }
+  wapp-trim {
+    <script src="script.js"></script>
+    </body>
+    </html>
+  }
+  incr G(cnt)
+}
+
+proc wapp-default {} {
+  generate_main_page
+}
+
+proc wapp-page-control {} {
+  global G
+  foreach v {platform test tcl jobs keep msvc} {
+    if {[wapp-param-exists control_$v]} {
+      set G($v) [wapp-param control_$v]
+    } else {
+      set G($v) 0
+    }
+  }
+
+  if {[wapp-param-exists control_go]} {
+    # This is an actual "run test" command, not just a change of 
+    # configuration!
+    set_test_array
+    set ::G(state) "running"
+  }
+
+  if {$::G(state) == "running"} {
+    do_some_stuff
+  }
+
+  wapp-redirect /
+}
+
+proc wapp-page-style.css {} {
+  wapp-subst {
+    .div {
+      border: 3px groove #444444;
+      margin: 1em;
+      padding: 1em;
+    }
+
+    .warning {
+      text-align:center;
+      color: red;
+      font-size: 2em;
+      font-weight: bold;
+    }
+
+    .right {
+    }
+
+    .testfield {
+      padding-right: 10ex;
+    }
+
+    .testwait {}
+    .testrunning { color: blue }
+    .testdone { color: green }
+    .testfail { color: red }
+  }
+}
+
+proc wapp-page-script.js {} {
+
+  set tcl $::G(tcl)
+  set keep $::G(keep)
+  set msvc $::G(msvc)
+  
+  wapp-subst {
+    var lElem = \["control_platform", "control_test", "control_msvc", "control_jobs"\];
+    lElem.forEach(function(e) {
+      var elem = document.getElementById(e);
+      elem.addEventListener("change", function() { control.submit() } );
+    })
+
+    elem = document.getElementById("control_tcl");
+    elem.value = "%string($tcl)"
+
+    elem = document.getElementById("control_keep");
+    elem.checked = %string($keep);
+
+    elem = document.getElementById("control_msvc");
+    elem.checked = %string($msvc);
+  }
+
+  if {$::G(state)!="config"} {
+    wapp-subst {
+      var lElem = \["control_platform", "control_test", 
+          "control_tcl", "control_keep", "control_msvc", "control_go"
+      \];
+      lElem.forEach(function(e) {
+        var elem = document.getElementById(e);
+        elem.disabled = true;
+      })
+    }
+  }
+}
+
+proc wapp-page-env {} {
+  wapp-allow-xorigin-params
+  wapp-trim {
+    <h1>Wapp Environment</h1>\n<pre>
+    <pre>%html([wapp-debug-env])</pre>
+  }
+}
+
+proc wapp-page-log {} {
+  set log [string range [wapp-param REQUEST_URI] 5 end]
+  set fd [open $log]
+  set data [read $fd]
+  close $fd
+  wapp-trim {
+    <pre>
+    %html($data)
+    </pre>
+  }
+}
+
+wapp-start $argv
+