# process.test -- # # This file contains a collection of tests for the tcl::process ensemble. # Sourcing this file into Tcl runs the tests and generates output for # errors. No output means no errors were found. # # Copyright © 2017 Frederic Bonnet # See the file "license.terms" for information on usage and redistribution of # this file, and for a DISCLAIMER OF ALL WARRANTIES. if {"::tcltest" ni [namespace children]} { package require tcltest 2.5 namespace import -force ::tcltest::* } # Utilities file delete [set path(test-signalfile) [makeFile {} test-signalfile]] set path(test-signalfile2) [makeFile {} test-signalfile2] # $path(sleep) time ?filename? -- sleep for time (in ms) and stop if it gets signaled (file gets deleted) set path(sleep) [makeFile { after [expr {[lindex $argv 0]*1000}] {set stop 1} if {[set fn [lindex $::argv 1]] ne ""} { close [open $fn w] proc check {} { if {![file exists $::fn]} { # exit signaled after 10 {set ::stop 2} } after 10 check } after 10 check } vwait stop exit } sleep] proc wait_for_file {fn {timeout 10000}} { if {![file exists $fn]} { set toev [after $timeout {set found 0}] proc check {fn} { if {[file exists $fn]} { set ::found 1 return } after 10 [list check $fn] } after 10 [list check $fn] vwait ::found after cancel $toev unset ::found } file exists $fn } proc signal_exit {fn {wait 1}} { # wait for until file created if expected: if {!$wait || [wait_for_file $fn]} { # delete file to signal exit for child-process: while {1} { if {![catch { file delete $fn } msg opt] || [lrange [dict get $opt -errorcode] 0 1] ne {POSIX EACCES} } break } } } set path(exit) [makeFile { exit [lindex $argv 0] } exit] # Basic syntax checking test process-1.1 {tcl::process command basic syntax} -returnCodes error -body { tcl::process } -result {wrong # args: should be "tcl::process subcommand ?arg ...?"} test process-1.2 {tcl::process subcommands} -returnCodes error -body { tcl::process ? } -match glob -result {unknown or ambiguous subcommand "?": must be autopurge, list, purge, or status} # Autopurge flag # - Default state test process-2.1 {autopurge default} -body { tcl::process autopurge } -result {1} # - Enabling autopurge test process-2.2 {enable autopurge} -body { tcl::process autopurge true tcl::process autopurge } -result {1} # - Disabling autopurge test process-2.3 {disable autopurge} -body { tcl::process autopurge false tcl::process autopurge } -result {0} -cleanup {tcl::process autopurge true} # Subprocess list & status test process-3.1 {empty subprocess list} -body { llength [tcl::process list] } -result {0} test process-3.2 {empty subprocess status} -body { dict size [tcl::process status] } -result {0} # Spawn subprocesses using [exec] # - One child test process-4.1 {exec one child} -body { tcl::process autopurge 0 set pid [exec [interpreter] $path(exit) 0 &] set list [tcl::process list] set statuses [tcl::process status -wait] set status [lindex [tcl::process status $pid] 1] expr { [llength $list] eq 1 && [lindex $list 0] eq $pid && [dict size $statuses] eq 1 && [dict get $statuses $pid] eq $status && $status eq 0 } } -result {1} -cleanup { tcl::process purge tcl::process autopurge 1 } # - Two children test process-4.2 {exec two children in parallel} -body { tcl::process autopurge 0 set pid1 [exec [interpreter] $path(exit) 0 &] set pid2 [exec [interpreter] $path(exit) 0 &] set list [tcl::process list] set statuses [tcl::process status -wait] set status1 [lindex [tcl::process status $pid1] 1] set status2 [lindex [tcl::process status $pid2] 1] expr { [llength $list] eq 2 && [lsearch $list $pid1] >= 0 && [lsearch $list $pid2] >= 0 && [dict size $statuses] eq 2 && [dict get $statuses $pid1] eq $status1 && [dict get $statuses $pid2] eq $status2 && $status1 eq 0 && $status2 eq 0 } } -result {1} -cleanup { tcl::process purge tcl::process autopurge 1 } # - 3-stage pipe test process-4.3 {exec 3-stage pipe} -body { tcl::process autopurge 0 set pids [exec \ [interpreter] $path(exit) 0 \ | [interpreter] $path(exit) 0 \ | [interpreter] $path(exit) 0 \ &] lassign $pids pid1 pid2 pid3 set list [tcl::process list] set statuses [tcl::process status -wait] set status1 [lindex [tcl::process status $pid1] 1] set status2 [lindex [tcl::process status $pid2] 1] set status3 [lindex [tcl::process status $pid3] 1] expr { [llength $pids] eq 3 && [llength $list] eq 3 && [lsearch $list $pid1] >= 0 && [lsearch $list $pid2] >= 0 && [lsearch $list $pid3] >= 0 && [dict size $statuses] eq 3 && [dict get $statuses $pid1] eq $status1 && [dict get $statuses $pid2] eq $status2 && [dict get $statuses $pid3] eq $status3 && $status1 eq 0 && $status2 eq 0 && $status3 eq 0 } } -result {1} -cleanup { tcl::process purge tcl::process autopurge 1 } # Spawn subprocesses using [open "|"] # - One child test process-5.1 {exec one child} -body { tcl::process autopurge 0 set f [open "|\"[interpreter]\" \"$path(exit)\" 0"] set pid [pid $f] set list [tcl::process list] set statuses [tcl::process status -wait] set status [lindex [tcl::process status $pid] 1] expr { [llength $list] eq 1 && [lindex $list 0] eq $pid && [dict size $statuses] eq 1 && [dict get $statuses $pid] eq $status && $status eq 0 } } -result {1} -cleanup { close $f tcl::process purge tcl::process autopurge 1 } # - Two children test process-5.2 {exec two children in parallel} -body { tcl::process autopurge 0 set f1 [open "|\"[interpreter]\" \"$path(exit)\" 0"] set f2 [open "|\"[interpreter]\" \"$path(exit)\" 0"] set pid1 [pid $f1] set pid2 [pid $f2] set list [tcl::process list] set statuses [tcl::process status -wait] set status1 [lindex [tcl::process status $pid1] 1] set status2 [lindex [tcl::process status $pid2] 1] expr { [llength $list] eq 2 && [lsearch $list $pid1] >= 0 && [lsearch $list $pid2] >= 0 && [dict size $statuses] eq 2 && [dict get $statuses $pid1] eq $status1 && [dict get $statuses $pid2] eq $status2 && $status1 eq 0 && $status2 eq 0 } } -result {1} -cleanup { close $f1 close $f2 tcl::process purge tcl::process autopurge 1 } # - 3-stage pipe test process-5.3 {exec 3-stage pipe} -body { tcl::process autopurge 0 set f [open "| \"[interpreter]\" \"$path(exit)\" 0 | \"[interpreter]\" \"$path(exit)\" 0 | \"[interpreter]\" \"$path(exit)\" 0 "] set pids [pid $f] lassign $pids pid1 pid2 pid3 set list [tcl::process list] set statuses [tcl::process status -wait] set status1 [lindex [tcl::process status $pid1] 1] set status2 [lindex [tcl::process status $pid2] 1] set status3 [lindex [tcl::process status $pid3] 1] expr { [llength $pids] eq 3 && [llength $list] eq 3 && [lsearch $list $pid1] >= 0 && [lsearch $list $pid2] >= 0 && [lsearch $list $pid3] >= 0 && [dict size $statuses] eq 3 && [dict get $statuses $pid1] eq $status1 && [dict get $statuses $pid2] eq $status2 && [dict get $statuses $pid3] eq $status3 && $status1 eq 0 && $status2 eq 0 && $status3 eq 0 } } -result {1} -cleanup { close $f tcl::process purge tcl::process autopurge 1 } # Async child status test process-6.1 {async status} -setup { signal_exit $path(test-signalfile) 0; # clean signal-file } -body { tcl::process autopurge 0 set pid [exec [interpreter] $path(sleep) 1 $path(test-signalfile) &] set status1 [lindex [tcl::process status $pid] 1] signal_exit $path(test-signalfile); # signal exit (stop sleep) set status2 [lindex [tcl::process status -wait $pid] 1] expr { $status1 eq {} && $status2 eq 0 } } -result {1} -cleanup { tcl::process purge tcl::process autopurge 1 } test process-6.2 {selective wait} -setup { signal_exit $path(test-signalfile) 0; # clean signal-files signal_exit $path(test-signalfile2) 0; } -body { tcl::process autopurge 0 # Child 1 sleeps 1s set pid1 [exec [interpreter] $path(sleep) 1 $path(test-signalfile) &] # Child 2 sleeps 1s set pid2 [exec [interpreter] $path(sleep) 2 $path(test-signalfile2) &] # Initial status set status1_1 [lindex [tcl::process status $pid1] 1] set status1_2 [lindex [tcl::process status $pid2] 1] # Wait until child 1 termination signal_exit $path(test-signalfile); # signal exit for pid1 (stop sleep) set status2_1 [lindex [tcl::process status -wait $pid1] 1] set status2_2 [lindex [tcl::process status $pid2] 1] # Wait until child 2 termination signal_exit $path(test-signalfile2); # signal exit for pid2 (stop sleep) set status3_2 [lindex [tcl::process status -wait $pid2] 1] set status3_1 [lindex [tcl::process status $pid1] 1] expr { $status1_1 eq {} && $status1_2 eq {} && $status2_1 eq 0 && $status2_2 eq {} && $status3_1 eq 0 && $status3_2 eq 0 } } -result {1} -cleanup { tcl::process purge tcl::process autopurge 1 } # Error codes test process-7.1 {normal exit} -body { tcl::process autopurge 0 set pid [exec [interpreter] $path(exit) 0 &] lindex [tcl::process status -wait $pid] 1 } -result {0} -cleanup { tcl::process purge tcl::process autopurge 1 } test process-7.2 {abnormal exit} -body { tcl::process autopurge 0 set pid [exec [interpreter] $path(exit) 1 &] lindex [tcl::process status -wait $pid] 1 } -match glob -result {1 {child process exited abnormally} {CHILDSTATUS * 1}} -cleanup { tcl::process purge tcl::process autopurge 1 } test process-7.3 {child killed} -constraints {win} -body { tcl::process autopurge 0 set pid [exec [interpreter] $path(exit) -1 &] lindex [tcl::process status -wait $pid] 1 } -match glob -result {1 {child killed: unknown signal} {CHILDKILLED * {unknown signal} {unknown signal}}} -cleanup { tcl::process purge tcl::process autopurge 1 } removeFile $path(exit) removeFile $path(sleep) rename wait_for_file {} rename signal_exit {} ::tcltest::cleanupTests return