diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /test/tester.tcl | |
| download | relic-master.tar.gz | |
Diffstat (limited to 'test/tester.tcl')
| -rw-r--r-- | test/tester.tcl | 1184 |
1 files changed, 1184 insertions, 0 deletions
diff --git a/test/tester.tcl b/test/tester.tcl new file mode 100644 index 0000000..a720ec1 --- /dev/null +++ b/test/tester.tcl @@ -0,0 +1,1184 @@ +# +# Copyright (c) 2006 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. +# +# Author contact information: +# drh@hwaci.com +# http://www.hwaci.com/drh/ +# +############################################################################ +# +# This is the main test script. To run a regression test, do this: +# +# tclsh ../test/tester.tcl ../bld/fossil +# +# Where ../test/tester.tcl is the name of this file and ../bld/fossil +# is the name of the executable to be tested. +# +# To run a subset of tests (i.e. only one or more of the test/*.test +# scripts), append the script base names as arguments: +# +# tclsh ../test/tester.tcl ../bld/fossil <script-basename>... +# + +# This testing system is a mess. It was poorly designed to begin with +# and has not been maintained. It is difficult to debug. It is completely +# disabled for the time being. +# +# If somebody wants to volunteer to fix it, then great. But without a +# volunteer to do so, it is disabled. +# +puts Ok +exit + +# We use some things introduced in 8.6 such as lmap. auto.def should +# have found us a suitable Tcl installation. +package require Tcl 8.6- + +set testfiledir [file normalize [file dirname [info script]]] +set testrundir [pwd] +set testdir [file normalize [file dirname $argv0]] +set fossilexe [file normalize [lindex $argv 0]] +set is_windows [expr {$::tcl_platform(platform) eq "windows"}] +set is_cygwin [regexp {^CYGWIN} $::tcl_platform(os)] + +if {$::is_windows} { + if {[string length [file extension $fossilexe]] == 0} { + append fossilexe .exe + } + set outside_fossil_repo [expr ![file exists "$::testfiledir\\..\\_FOSSIL_"]] +} else { + set outside_fossil_repo [expr ![file exists "$::testfiledir/../.fslckout"]] +} + +catch {exec $::fossilexe changes --changed} res +set dirty_ckout [string length $res] + +set argv [lrange $argv 1 end] + +set i [lsearch $argv -keep] +if {$i>=0} { + set KEEP 1 + set argv [lreplace $argv $i $i] +} else { + set KEEP 0 +} + +set i [lsearch $argv -halt] +if {$i>=0} { + set HALT 1 + set argv [lreplace $argv $i $i] +} else { + set HALT 0 +} + +set i [lsearch $argv -prot] +if {$i>=0} { + set PROT 1 + set argv [lreplace $argv $i $i] +} else { + set PROT 0 +} + +set i [lsearch $argv -verbose] +if {$i>=0} { + set VERBOSE 1 + set argv [lreplace $argv $i $i] +} else { + set VERBOSE 0 +} + +set i [lsearch $argv -quiet] +if {$i>=0} { + set QUIET 1 + set argv [lreplace $argv $i $i] +} else { + set QUIET 0 +} + +set i [lsearch $argv -strict] +if {$i>=0} { + set STRICT 1 + set argv [lreplace $argv $i $i] +} else { + set STRICT 0 +} + +if {[llength $argv]==0} { + foreach f [lsort [glob $testdir/*.test]] { + set base [file root [file tail $f]] + lappend argv $base + } +} + +# start protocol +# +proc protInit {cmd} { + if {$::PROT} { + set out [open [file join $::testrundir prot] w] + fconfigure $out -translation platform + puts $out "starting tests with: $cmd" + close $out + } +} + +# write protocol +# +proc protOut {msg {noQuiet 0}} { + if {$noQuiet || !$::QUIET} { + puts stdout $msg + } + if {$::PROT} { + set out [open [file join $::testrundir prot] a] + fconfigure $out -translation platform + puts $out $msg + close $out + } +} + +# write a dict with just enough formatting +# to make it human readable +# +proc protOutDict {dict {pattern *}} { + set longest [tcl::mathfunc::max 0 {*}[lmap key [dict keys $dict $pattern] {string length $key}]] + dict for {key value} $dict { + protOut [format "%-${longest}s = %s" $key $value] + } +} + + +# Run the Fossil program with the specified arguments. +# +# Consults the VERBOSE global variable to determine if +# diagnostics should be emitted when no error is seen. +# Sets the CODE and RESULT global variables for use in +# test expressions. +# +proc fossil {args} { + return [uplevel 1 fossil_maybe_answer [list ""] $args] +} + +# Run the Fossil program with the specified arguments +# and possibly answer the first prompt, if any. +# +# Consults the VERBOSE global variable to determine if +# diagnostics should be emitted when no error is seen. +# Sets the CODE and RESULT global variables for use in +# test expressions. +# +proc fossil_maybe_answer {answer args} { + global fossilexe + set cmd $fossilexe + set expectError 0 + set index [lsearch -exact $args -expectError] + if {$index != -1} { + set expectError 1 + set args [lreplace $args $index $index] + } + set keepNewline 0 + set index [lsearch -exact $args -keepNewline] + if {$index != -1} { + set keepNewline 1 + set args [lreplace $args $index $index] + } + set whatIf 0 + set index [lsearch -exact $args -whatIf] + if {$index != -1} { + set whatIf 1 + set args [lreplace $args $index $index] + } + foreach a $args { + lappend cmd $a + } + protOut $cmd + + flush stdout + if {$whatIf} { + protOut [pwd]; protOut $answer + set result WHAT-IF-MODE; set rc 42 + } else { + if {[string length $answer] > 0} { + protOut $answer + set prompt_file [file join $::tempPath fossil_prompt_answer] + write_file $prompt_file $answer\n + set execCmd [list eval exec] + if {$keepNewline} {lappend execCmd -keepnewline} + lappend execCmd $cmd <$prompt_file + set rc [catch $execCmd result] + file delete $prompt_file + } else { + set execCmd [list eval exec] + if {$keepNewline} {lappend execCmd -keepnewline} + lappend execCmd $cmd + set rc [catch $execCmd result] + } + } + set ab(str) {child process exited abnormally} + set ab(len) [string length $ab(str)] + set ab(off) [expr {$ab(len) - 1}] + if {$rc && $expectError && \ + [string range $result end-$ab(off) end] eq $ab(str)} { + set result [string range $result 0 end-$ab(len)] + } + global RESULT CODE + set CODE $rc + if {!$whatIf} { + if {($rc && !$expectError) || (!$rc && $expectError)} { + protOut "ERROR ($rc): $result" 1 + } elseif {$::VERBOSE} { + protOut "RESULT ($rc): $result" + } + } + set RESULT $result +} + +# Read a file into memory. +# +proc read_file {filename} { + set in [open $filename r] + fconfigure $in -translation binary + set txt [read $in [file size $filename]] + close $in + return $txt +} + +# Write a file to disk +# +proc write_file {filename txt} { + set out [open $filename w] + fconfigure $out -translation binary + puts -nonewline $out $txt + close $out +} +proc write_file_indented {filename txt} { + write_file $filename [string trim [string map [list "\n " \n] $txt]]\n +} + +# Returns the list of all supported versionable settings. +# +proc get_versionable_settings {} { + # + # TODO: If the list of supported versionable settings in "db.c" is modified, + # this list (and procedure) most likely needs to be modified as well. + # + set result [list \ + binary-glob \ + clean-glob \ + crlf-glob \ + crnl-glob \ + dotfiles \ + empty-dirs \ + encoding-glob \ + ignore-glob \ + keep-glob \ + manifest] + + return [lsort -dictionary $result] +} + +# Returns the list of all supported settings. +# +proc get_all_settings {} { + # + # TODO: If the list of supported settings in "db.c" is modified, this list + # (and procedure) most likely needs to be modified as well. + # + set result [list \ + access-log \ + admin-log \ + allow-symlinks \ + auto-captcha \ + auto-hyperlink \ + auto-hyperlink-delay \ + auto-hyperlink-mouseover \ + auto-shun \ + autosync \ + autosync-tries \ + backoffice-disable \ + backoffice-logfile \ + backoffice-nodelay \ + binary-glob \ + case-sensitive \ + chat-alert-sound \ + chat-initial-history \ + chat-inline-images \ + chat-keep-count \ + chat-keep-days \ + chat-poll-timeout \ + chat-timeline-user \ + clean-glob \ + clearsign \ + comment-format \ + crlf-glob \ + crnl-glob \ + default-csp \ + default-perms \ + default-skin \ + diff-binary \ + diff-command \ + dont-commit \ + dont-push \ + dotfiles \ + editor \ + email-admin \ + email-listid \ + email-renew-interval \ + email-self \ + email-send-command \ + email-send-db \ + email-send-dir \ + email-send-method \ + email-send-relayhost \ + email-subname \ + email-url \ + empty-dirs \ + encoding-glob \ + exec-rel-paths \ + fileedit-glob \ + forbid-delta-manifests \ + forum-close-policy \ + forum-title \ + gdiff-command \ + gmerge-command \ + hash-digits \ + hooks \ + http-port \ + https-login \ + ignore-glob \ + keep-glob \ + large-file-size \ + localauth \ + lock-timeout \ + main-branch \ + mainmenu \ + manifest \ + max-cache-entry \ + max-loadavg \ + max-upload \ + mimetypes \ + mtime-changes \ + mv-rm-files \ + pgp-command \ + preferred-diff-type \ + proxy \ + raw-bgcolor \ + redirect-to-https \ + relative-paths \ + repo-cksum \ + repolist-skin \ + robot-restrict \ + robots-txt \ + safe-html \ + self-pw-reset \ + self-register \ + sitemap-extra \ + ssh-command \ + ssl-ca-location \ + ssl-identity \ + tclsh \ + th1-setup \ + th1-uri-regexp \ + ticket-default-report \ + timeline-hard-newlines \ + timeline-plaintext \ + timeline-truncate-at-blank \ + timeline-tslink-info \ + timeline-utc \ + user-color-map \ + verify-comments \ + uv-sync \ + vuln-report \ + web-browser] + + fossil test-th-eval "hasfeature legacyMvRm" + + if {[normalize_result] eq "1"} { + lappend result mv-rm-files + } + + fossil test-th-eval "hasfeature tcl" + + if {[normalize_result] eq "1"} { + lappend result tcl tcl-setup + } + + fossil test-th-eval "hasfeature th1Docs" + + if {[normalize_result] eq "1"} { + lappend result th1-docs + } + + fossil test-th-eval "hasfeature th1Hooks" + + if {[normalize_result] eq "1"} { + lappend result th1-hooks + } + + return [lsort -dictionary $result] +} + +# Return true if two files are the same +# +proc same_file {a b} { + set x [read_file $a] + regsub -all { +\n} $x \n x + set y [read_file $b] + regsub -all { +\n} $y \n y + if {$x == $y} { + return 1 + } else { + if {$::VERBOSE} { + protOut "NOT_SAME_FILE($a): \{\n$x\n\}" + protOut "NOT_SAME_FILE($b): \{\n$y\n\}" + } + return 0 + } +} + +# Return true if two strings refer to the +# same uuid. That is, the shorter is a prefix +# of the longer. +# +proc same_uuid {a b} { + set na [string length $a] + set nb [string length $b] + if {$na == $nb} { + return [expr {$a eq $b}] + } + if {$na < $nb} { + return [string match "$a*" $b] + } + return [string match "$b*" $a] +} + +# Return a prefix of a uuid, defaulting to 10 chars. +# +proc short_uuid {uuid {len 10}} { + string range $uuid 0 $len-1 +} + + +proc require_no_open_checkout {} { + if {[info exists ::env(FOSSIL_TEST_DANGEROUS_IGNORE_OPEN_CHECKOUT)] && \ + $::env(FOSSIL_TEST_DANGEROUS_IGNORE_OPEN_CHECKOUT) eq "YES_DO_IT"} { + return + } + catch {exec $::fossilexe info} res + if {[regexp {local-root:} $res]} { + global skipped_tests testfile + lappend skipped_tests $testfile + set projectName <unknown> + set localRoot <unknown> + regexp -line -- {^project-name: (.*)$} $res dummy projectName + set projectName [string trim $projectName] + regexp -line -- {^local-root: (.*)$} $res dummy localRoot + set localRoot [string trim $localRoot] + error "Detected an open checkout of project \"$projectName\",\ +rooted at \"$localRoot\", testing halted." + } +} + +proc get_script_or_fail {} { + set fileName [file normalize [info script]] + if {[string length $fileName] == 0 || ![file exists $fileName]} { + error "Failed to obtain the file name of the test being run." + } + return $fileName +} + +proc robust_delete { path {force ""} } { + set error "unknown error" + for {set try 0} {$try < 10} {incr try} { + if {$force eq "YES_DO_IT"} { + if {[catch {file delete -force $path} error] == 0} { + return + } + } else { + if {[catch {file delete $path} error] == 0} { + return + } + } + after [expr {$try * 100}] + } + error "Could not delete \"$path\", error: $error" +} + +proc test_cleanup_then_return {} { + global skipped_tests testfile + lappend skipped_tests $testfile + uplevel 1 [list test_cleanup] + return -code return +} + +proc test_cleanup {} { + if {$::KEEP} { + # To avoid errors with require_no_open_checkout, cd out of here. + if {[info exists ::tempSavedPwd]} {cd $::tempSavedPwd; unset ::tempSavedPwd} + return + } + if {![info exists ::tempRepoPath]} {return} + if {![file exists $::tempRepoPath]} {return} + if {![file isdirectory $::tempRepoPath]} {return} + set tempPathEnd [expr {[string length $::tempPath] - 1}] + if {[string length $::tempPath] == 0 || \ + [string range $::tempRepoPath 0 $tempPathEnd] ne $::tempPath} { + error "Temporary repository path has wrong parent during cleanup." + } + if {[info exists ::tempSavedPwd]} {cd $::tempSavedPwd; unset ::tempSavedPwd} + # First, attempt to delete the specific temporary repository directories + # for this test file. + set scriptName [file tail [get_script_or_fail]] + foreach repoSeed $::tempRepoSeeds { + set repoPath [file join $::tempRepoPath $repoSeed $scriptName] + robust_delete $repoPath YES_DO_IT; # FORCE, arbitrary children. + set seedPath [file join $::tempRepoPath $repoSeed] + robust_delete $seedPath; # NO FORCE. + } + # Next, attempt to gracefully delete the temporary repository directory + # for this process. + robust_delete $::tempRepoPath + # Finally, attempt to gracefully delete the temporary home directory, + # unless forbidden by external forces. + if {![info exists ::tempKeepHome]} {delete_temporary_home} +} + +proc delete_temporary_home {} { + if {$::KEEP} {return}; # All cleanup disabled? + if {$::is_windows || $::is_cygwin} { + robust_delete [file join $::tempHomePath _fossil] + } else { + robust_delete [file join $::tempHomePath .fossil] + } + robust_delete $::tempHomePath +} + +proc is_home_elsewhere {} { + return [expr {[info exists ::env(FOSSIL_HOME)] && \ + $::env(FOSSIL_HOME) eq $::tempHomePath}] +} + +proc set_home_to_elsewhere {} { + # + # Fossil will write data on $HOME (or $FOSSIL_HOME). We need not + # to clutter the real $HOME (or $FOSSIL_HOME) of the test caller. + # + if {[is_home_elsewhere]} {return} + set ::env(FOSSIL_HOME) $::tempHomePath +} + +# +# Create and open a new Fossil repository and clean the checkout +# +proc test_setup {{filename ".rep.fossil"}} { + set_home_to_elsewhere + if {![info exists ::tempRepoPath]} { + set ::tempRepoPath [file join $::tempPath repo_[pid]] + } + set repoSeed [appendArgs [string trim [clock seconds] -] _ [getSeqNo]] + lappend ::tempRepoSeeds $repoSeed + set repoPath [file join \ + $::tempRepoPath $repoSeed [file tail [get_script_or_fail]]] + if {[catch { + file mkdir $repoPath + } error] != 0} { + error "Could not make directory \"$repoPath\",\ +please set TEMP variable in environment, error: $error" + } + if {![info exists ::tempSavedPwd]} {set ::tempSavedPwd [pwd]}; cd $repoPath + if {[string length $filename] > 0} { + exec $::fossilexe new $filename + exec $::fossilexe open $filename + exec $::fossilexe set mtime-changes off + } + return $repoPath +} + +# This procedure only returns non-zero if the Tcl integration feature was +# enabled at compile-time and is now enabled at runtime. +proc is_tcl_usable_by_fossil {} { + fossil test-th-eval "hasfeature tcl" + if {[normalize_result] ne "1"} {return 0} + fossil test-th-eval "setting tcl" + if {[normalize_result] eq "1"} {return 1} + fossil test-th-eval --open-config "setting tcl" + if {[normalize_result] eq "1"} {return 1} + return [info exists ::env(TH1_ENABLE_TCL)] +} + +# This procedure only returns non-zero if the TH1 hooks feature was enabled +# at compile-time and is now enabled at runtime. +proc are_th1_hooks_usable_by_fossil {} { + fossil test-th-eval "hasfeature th1Hooks" + if {[normalize_result] ne "1"} {return 0} + fossil test-th-eval "setting th1-hooks" + if {[normalize_result] eq "1"} {return 1} + fossil test-th-eval --open-config "setting th1-hooks" + if {[normalize_result] eq "1"} {return 1} + return [info exists ::env(TH1_ENABLE_HOOKS)] +} + +# Run the given command script inside the Fossil source repo checkout. +# +# Callers of this function must ensure two things: +# +# 1. This test run is in fact being done from within a Fossil repo +# checkout directory. If you are unsure, test $::outside_fossil_repo +# or call one of the test_* wrappers below which do that for you. +# +# As a rule, you should not be calling this function directly! +# +# 2. This test run is being done from a repo checkout directory that +# doesn't have any uncommitted changes. If it does, that affects the +# output of any test based on the output of "fossil status", +# "... diff", etc., which is likely to make the test appear to fail. +# If you must call this function directly, test $::dirty_ckout and +# skip the call if it's true. The test_* wrappers do this for you. +# +# 3. The test does NOT modify the Fossil checkout tree in any way. +proc run_in_checkout { script {dir ""} } { + if {[string length $dir] == 0} {set dir $::testfiledir} + set savedPwd [pwd]; cd $dir + set code [catch { + uplevel 1 $script + } result] + cd $savedPwd; unset savedPwd + return -code $code $result +} + +# Wrapper for the above function pair. The tscript parameter is an +# optional post-run test script. Some callers choose instead to put +# the tests inline with the rscript commands. +# +# Be sure to adhere to the requirements of run_in_checkout! +proc test_block_in_checkout { name rscript {tscript ""} } { + if {$::outside_fossil_repo || $::dirty_ckout} { + set $::CODE 0 + set $::RESULT "" + } else { + uplevel 1 [list run_in_checkout $rscript] + if {[string length $tscript] == 0} { + return "" + } else { + set code [catch { + uplevel 1 $tscript + } result] + return -code $code $result + } + } +} + +# Single-test wrapper for the above. +proc test_in_checkout { name rscript tscript } { + return test_block_in_checkout name rscript { + test $name $tscript + } +} + +# Normalize file status lists (like those returned by 'fossil changes') +# so they can be compared using simple string comparison +# +proc normalize_status_list {list} { + set normalized [list] + set matches [regexp -all -inline -line {^\s*([A-Z_]+:?)\x20+(\S.*)$} $list] + foreach {_ status file} $matches { + lappend normalized [list $status [string trim $file]] + } + set normalized [lsort -index 1 $normalized] + return $normalized +} + +# Perform a test comparing two status lists +# +proc test_status_list {name result expected {constraints ""}} { + set expected [normalize_status_list $expected] + set result [normalize_status_list $result] + if {$result eq $expected} { + test $name 1 $constraints + } else { + protOut " Expected:\n [join $expected "\n "]" 1 + protOut " Got:\n [join $result "\n "]" 1 + test $name 0 $constraints + } +} + +# Perform a test on the contents of a file +# +proc test_file_contents {name path expected {constraints ""}} { + if {[file exists $path]} { + set result [read_file $path] + set passed [expr {$result eq $expected}] + if {!$passed} { + set expectedLines [split $expected "\n"] + set resultLines [split $result "\n"] + protOut " Expected:\n [join $expectedLines "\n "]" 1 + protOut " Got:\n [join $resultLines "\n "]" 1 + } + } else { + set passed 0 + protOut " File does not exist: $path" 1 + } + test $name $passed $constraints +} + +# Append all arguments into a single value and then returns it. +# +proc appendArgs {args} { + eval append result $args +} + +# Returns the value of the specified environment variable -OR- any empty +# string if it does not exist. +# +proc getEnvironmentVariable { name } { + return [expr {[info exists ::env($name)] ? $::env($name) : ""}] +} + +# Returns a usable temporary directory -OR- fails the testing process. +# +proc getTemporaryPath {} { + # + # NOTE: Build the list of "temporary directory" environment variables + # to check, including all reasonable "cases" of the environment + # variable names. + # + set names [list] + + # + # TODO: Add more here, if necessary. + # + foreach name [list FOSSIL_TEST_TEMP FOSSIL_TEMP TEMP TMP] { + lappend names [string toupper $name] [string tolower $name] \ + [string totitle $name] + } + + # + # NOTE: Check if we can use any of the environment variables. + # + foreach name $names { + set value [getEnvironmentVariable $name] + + if {[string length $value] > 0} { + set value [file normalize $value] + + if {[file exists $value] && [file isdirectory $value]} { + return $value + } + } + } + + # + # NOTE: On non-Windows systems, fallback to /tmp if it is usable. + # + if {!$::is_windows} { + set value /tmp + + if {[file exists $value] && [file isdirectory $value]} { + return $value + } + } + + # + # NOTE: There must be a usable temporary directory to continue testing. + # + error "Cannot find a usable temporary directory, testing halted." +} + +# Return the name of the versioned settings file containing the TH1 +# setup script. +# +proc getTh1SetupFileName {} { + # + # NOTE: This uses the "testdir" global variable provided by the + # test suite; alternatively, the root of the source tree + # could be obtained directly from Fossil. + # + return [file normalize [file join .fossil-settings th1-setup]] +} + +# Return the saved name of the versioned settings file containing +# the TH1 setup script. +# +proc getSavedTh1SetupFileName {} { + return [appendArgs [getTh1SetupFileName] . [pid]] +} + +# Sets the TH1 setup script to the one provided. Prior to calling +# this, the [saveTh1SetupFile] procedure should be called in order to +# preserve the existing TH1 setup script. Prior to completing the test, +# the [restoreTh1SetupFile] procedure should be called to restore the +# original TH1 setup script. +# +proc writeTh1SetupFile { data } { + set fileName [getTh1SetupFileName] + file mkdir [file dirname $fileName] + return [write_file $fileName $data] +} + +# Saves the TH1 setup script file by renaming it, based on the current +# process ID. +# +proc saveTh1SetupFile {} { + set oldFileName [getTh1SetupFileName] + if {[file exists $oldFileName]} { + set newFileName [getSavedTh1SetupFileName] + catch {file delete $newFileName} + file rename $oldFileName $newFileName + } +} + +# Restores the original TH1 setup script file by renaming it back, based +# on the current process ID. +# +proc restoreTh1SetupFile {} { + set oldFileName [getSavedTh1SetupFileName] + set newFileName [getTh1SetupFileName] + if {[file exists $oldFileName]} { + catch {file delete $newFileName} + file rename $oldFileName $newFileName + } else { + # + # NOTE: There was no TH1 setup script file, delete the test one. + # + file delete $newFileName + } +} + +# Perform a test +# +set test_count 0 +proc test {name expr {constraints ""}} { + global bad_test ignored_test test_count RESULT + incr test_count + set knownBug [expr {"knownBug" in $constraints}] + set r [uplevel 1 [list expr $expr]] + if {$r} { + if {$knownBug && !$::STRICT} { + protOut "test $name OK (knownBug)?" + } else { + protOut "test $name OK" + } + } else { + if {$knownBug && !$::STRICT} { + protOut "test $name FAILED (knownBug)!" 1 + lappend ignored_test $name + } else { + protOut "test $name FAILED!" 1 + if {$::QUIET} {protOut "RESULT: $RESULT" 1} + lappend bad_test $name + if {$::HALT} {exit 1} + } + } +} +set bad_test {} +set ignored_test {} +set skipped_tests {} + +# Return a random string N characters long. +# +set vocabulary 01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +append vocabulary " ()*^!.eeeeeeeeaaaaattiioo " +set nvocabulary [string length $vocabulary] +proc rand_str {N} { + global vocabulary nvocabulary + set out {} + while {$N>0} { + incr N -1 + set i [expr {int(rand()*$nvocabulary)}] + append out [string index $vocabulary $i] + } + return $out +} + +# Make random changes to a file. +# +# The file is divided into blocks of $blocksize lines each. The first +# block is number 0. Changes are only made within blocks where +# the block number divided by $count has a remainder of $index. +# +# For any given line that mets the block count criteria, the probably +# of a change is $prob +# +# Changes do not add or remove newlines +# +proc random_changes {body blocksize count index prob} { + set out {} + set blockno 0 + set lineno -1 + foreach line [split $body \n] { + incr lineno + if {$lineno==$blocksize} { + incr blockno + set lineno 0 + } + if {$blockno%$count==$index && rand()<$prob} { + set n [string length $line] + if {$n>5 && rand()<0.5} { + # delete part of the line + set n [expr {int(rand()*$n)}] + set i [expr {int(rand()*$n)}] + set k [expr {$i+$n}] + set line [string range $line 0 $i][string range $line $k end] + } else { + # insert something into the line + set stuff [rand_str [expr {int(rand()*($n-5))-1}]] + set i [expr {int(rand()*$n)}] + set ip1 [expr {$i+1}] + set line [string range $line 0 $i]$stuff[string range $line $ip1 end] + } + } + append out \n$line + } + return [string range $out 1 end] +} + +# This procedure executes the "fossil server" command. The return value +# is a list comprised of the new process identifier and the port on which +# the server started. The varName argument refers to a variable +# where the "stop argument" is to be stored. This value must eventually be +# passed to the [test_stop_server] procedure. +proc test_start_server { repository {varName ""} } { + global fossilexe tempPath + set command [list exec $fossilexe server --localhost] + if {[string length $varName] > 0} { + upvar 1 $varName stopArg + } + if {$::is_windows} { + set stopArg [file join [getTemporaryPath] [appendArgs \ + [string trim [clock seconds] -] _ [getSeqNo] .stopper]] + lappend command --stopper $stopArg + } + set outFileName [file join $tempPath [appendArgs \ + fossil_server_ [string trim [clock seconds] -] _ \ + [getSeqNo]]].out + lappend command $repository >&$outFileName & + set pid [eval $command] + if {!$::is_windows} { + set stopArg $pid + } + after 1000; # output might not be there yet + set output [read_file $outFileName] + if {![regexp {Listening.*TCP port (\d+)} $output dummy port]} { + puts stdout "Could not detect Fossil server port, using default..." + set port 8080; # return the default port just in case + } + return [list $pid $port $outFileName] +} + +# This procedure stops a Fossil server instance that was previously started +# by the [test_start_server] procedure. The value of the "stop argument" +# will vary by platform as will the exact method used to stop the server. +# The fileName argument is the name of a temporary output file to delete. +proc test_stop_server { stopArg pid fileName } { + if {$::is_windows} { + # + # NOTE: On Windows, the "stop argument" must be the name of a file + # that does NOT already exist. + # + if {[string length $stopArg] > 0 && \ + ![file exists $stopArg] && \ + [catch {write_file $stopArg [clock seconds]}] == 0} { + while {1} { + if {[catch { + # + # NOTE: Using the TaskList utility requires Windows XP or + # later. + # + exec tasklist.exe /FI "PID eq $pid" + } result] != 0 || ![regexp -- " $pid " $result]} { + break + } + after 1000; # wait a bit... + } + file delete $stopArg + if {[string length $fileName] > 0} { + file delete $fileName + } + return true + } + } else { + # + # NOTE: On Unix, the "stop argument" must be an integer identifier + # that refers to an existing process. + # + if {[regexp {^(?:-)?\d+$} $stopArg] && \ + [catch {exec kill -TERM $stopArg}] == 0} { + while {1} { + if {[catch { + # + # TODO: Is this portable to all the supported variants of + # Unix? It should be, it's POSIX. + # + exec ps -p $pid + } result] != 0 || ![regexp -- "(?:^$pid| $pid) " $result]} { + break + } + after 1000; # wait a bit... + } + if {[string length $fileName] > 0} { + file delete $fileName + } + return true + } + } + return false +} + +# Executes the "fossil http" command. The entire content of the HTTP request +# is read from the data file name, with [subst] being performed on it prior to +# submission. Temporary input and output files are created and deleted. The +# result will be the contents of the temoprary output file. +proc test_fossil_http { repository dataFileName url } { + set suffix [appendArgs [pid] - [getSeqNo] - [clock seconds] .txt] + set inFileName [file join $::tempPath [appendArgs test-http-in- $suffix]] + set outFileName [file join $::tempPath [appendArgs test-http-out- $suffix]] + set data [subst [read_file $dataFileName]] + + write_file $inFileName $data + + fossil http --in $inFileName --out $outFileName --ipaddr 127.0.0.1 \ + $repository --localauth --th-trace -expectError + + set result [expr {[file exists $outFileName] ? [read_file $outFileName] : ""}] + + if {1} { + catch {file delete $inFileName} + catch {file delete $outFileName} + } + + return $result +} + +# obtains and increments a "sequence number" for this test run. +proc getSeqNo {} { + upvar #0 seqNo seqNo + if {![info exists seqNo]} { + set seqNo 0 + } + return [incr seqNo] +} + +# fixup the whitespace in the result to make it easier to compare. +proc normalize_result {} { + return [string map [list \r\n \n] [string trim $::RESULT]] +} + +# fixup the line-endings in the result to make it easier to compare. +proc normalize_result_no_trim {} { + return [string map [list \r\n \n] $::RESULT] +} + +# returns the first line of the normalized result. +proc first_data_line {} { + return [lindex [split [normalize_result] \n] 0] +} + +# returns the second line of the normalized result. +proc second_data_line {} { + return [lindex [split [normalize_result] \n] 1] +} + +# returns the third line of the normalized result. +proc third_data_line {} { + return [lindex [split [normalize_result] \n] 2] +} + +# returns the last line of the normalized result. +proc last_data_line {} { + return [lindex [split [normalize_result] \n] end] +} + +# returns the second to last line of the normalized result. +proc next_to_last_data_line {} { + return [lindex [split [normalize_result] \n] end-1] +} + +# returns the third to last line of the normalized result. +proc third_to_last_data_line {} { + return [lindex [split [normalize_result] \n] end-2] +} + +set tempPath [getTemporaryPath] + +if {$is_windows} { + set tempPath [string map [list \\ /] $tempPath] +} + +if {[catch { + set tempFile [file join $tempPath temporary.txt] + write_file $tempFile [clock seconds]; file delete $tempFile +} error] != 0} { + error "Could not write file \"$tempFile\" in directory \"$tempPath\",\ +please set TEMP variable in environment, error: $error" +} + +set tempHomePath [file join $tempPath home_[pid]] + +# Close stdin to avoid errors on wrapped text for narrow terminals. +# Closing stdin means that terminal detection returns 0 width, in turn +# causing the relvant strings to be printed on a single line. +# However, closing stdin makes file descriptor 0 avaailable on some systems +# and/or TCL implementations, which triggers fossil to complain about opening +# databases using fd 0. Avoid this by opening the script, consuming fd 0. +close stdin +set possibly_fd0 [open [info script] r] + +if {[catch { + file mkdir $tempHomePath +} error] != 0} { + error "Could not make directory \"$tempHomePath\",\ +please set TEMP variable in environment, error: $error" +} + + +protInit $fossilexe +set ::tempKeepHome 1 + +# Start in tempHomePath to help avoid errors with require_no_open_checkout +set startPwd [pwd] +cd $tempHomePath + +foreach testfile $argv { + protOut "***** $testfile ******" + if { [catch {source $testdir/$testfile.test} testerror testopts] } { + test test-framework-$testfile 0 + protOut "!!!!! $testfile: $testerror" + protOutDict $testopts" + } else { + test test-framework-$testfile 1 + } + protOut "***** End of $testfile: [llength $bad_test] errors so far ******" +} +cd $startPwd +unset ::tempKeepHome; delete_temporary_home + +# Clean up the file descriptor +close $possibly_fd0 + +set nErr [llength $bad_test] +if {$nErr>0 || !$::QUIET} { + protOut "***** Final results: $nErr errors out of $test_count tests" 1 +} +if {$nErr>0} { + protOut "***** Considered failures: $bad_test" 1 +} +set nErr [llength $ignored_test] +if {$nErr>0 || !$::QUIET} { + protOut "***** Ignored results: $nErr ignored errors out of $test_count tests" 1 +} +if {$nErr>0} { + protOut "***** Ignored failures: $ignored_test" 1 +} +set nSkipped [llength $skipped_tests] +if {$nSkipped>0} { + protOut "***** Skipped tests: $skipped_tests" 1 +} +if {$bad_test>0} { + exit 1 +} |
