# testutils.tcl -- # # This file is sourced by each test file when invoking "tcltest::loadTestedCommands". # It implements the testutils mechanism which is used to import utility procs # into test files that need them. # # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # # DOCUMENTATION FOR TEST AUTHORS AND MAINTAINERS # # The testutils mechanism is documented in the separate file "testutils.GUIDE", # which is placed in the same directory as this file "testutils.tcl". # namespace eval ::tk::test { # # The namespace ::tk::test itself doesn't contain any procs or variables. # The contents of this namespace exist solely in child namespaces that # are defined hereafter. # # Each child namespace represents a functional area, also called "domain". # } namespace eval ::tk::test::generic { proc assert {expr} { if {! [uplevel 1 [list expr $expr]]} { return -code error "assertion failed: \"[uplevel 1 [list subst -nocommands $expr]]\"" } } # controlPointerWarpTiming -- # # This proc is intended to ensure that the (mouse) pointer has actually # been moved to its new position after a Tk test issued: # # [event generate $w $event -warp 1 ...] # # It takes care of the following timing details of pointer warping: # # a. Allow pointer warping to happen if it was scheduled for execution at # idle time. This happens synchronously if $w refers to the # whole screen or if the -when option to [event generate] is "now". # # b. Work around a race condition associated with OS notification of # mouse motion on Windows. # # When calling [event generate $w $event -warp 1 ...], the following # sequence occurs: # - At some point in the processing of this command, either via a # synchronous execution path, or asynchronously at idle time, Tk calls # an OS function* to carry out the mouse cursor motion. # - Tk has previously registered a callback function** with the OS, for # the OS to call in order to notify Tk when a mouse move is completed. # - Tk doesn't wait for the callback function to receive the notification # from the OS, but continues processing. This suits most use cases # because usually the notification arrives fast enough (within a few tens # of microseconds). However ... # - A problem arises if Tk performs some processing, immediately following # up on [event generate $w $event -warp 1 ...], and that processing # relies on the mouse pointer having actually moved. If such processing # happens just before the notification from the OS has been received, # Tk will be using not yet updated info (e.g. mouse coordinates). # # Hickup, choke etc ... ! # # * the function SendInput() of the Win32 API # ** the callback function is TkWinChildProc() # # This timing issue can be addressed by putting the Tk process on hold # (do nothing at all) for a somewhat extended amount of time, while # letting the OS complete its job in the meantime. This is what is # accomplished by calling [after ms]. # # ---- # For the history of this issue please refer to Tk ticket [69b48f427e], # specifically the comment on 2019-10-27 14:24:26. # # # Beware: there are cases, not (yet) exercised by the Tk test suite, where # [controlPointerWarpTiming] doesn't ensure the new position of the pointer. # For example, when issued under Tk8.7+, if the value for the -when option # to [event generate $w] is not "now", and $w refers to a Tk window, i.e. not # the whole screen. # proc controlPointerWarpTiming {{duration 50}} { update idletasks ;# see a. above if {[tk windowingsystem] eq "win32"} { after $duration ;# see b. above } } proc deleteWindows {} { destroy {*}[winfo children .] # This update is needed to avoid intermittent failures on macOS in unixEmbed.test # with the (GitHub Actions) CI runner. # Reason for the failures is unclear but could have to do with window ids being deleted # after the destroy command returns. The detailed mechanism of such delayed deletions # is not understood, but it appears that this update prevents the test failures. update } proc fixfocus {} { catch {destroy .focus} toplevel .focus wm geometry .focus +0+0 entry .focus.e .focus.e insert 0 "fixfocus" pack .focus.e update focus -force .focus.e destroy .focus } proc loadTkCommand {} { variable TkLoadCmd if {! [info exists TkLoadCmd]} { foreach pkg [info loaded] { if {[lindex $pkg 1] eq "Tk"} { set TkLoadCmd [list load {*}$pkg] break } } } return $TkLoadCmd } # Suspend script execution for a given amount of time, but continue # processing events. proc pause {ms} { variable _pause set num [incr _pause(count)] set _pause($num) 1 after $ms [list unset [namespace current]::_pause($num)] vwait [namespace current]::_pause($num) } # On macOS windows are not allowed to overlap the menubar at the top of the # screen or the dock. So tests which move a window and then check whether it # got moved to the requested location should use a y coordinate larger than the # height of the menubar (normally 23 pixels) and an x coordinate larger than the # width of the dock, if it happens to be on the left. # The C-level command "testmenubarheight" deals with this issue but it may # not be available on each platform. Therefore, provide a fallback here. if {[llength [info commands testmenubarheight]] == 0} { if {[tk windowingsystem] ne "aqua"} { # Windows may overlap the menubar proc testmenubarheight {} { return 0 } } else { # Windows may not overlap the menubar proc testmenubarheight {} { return 30 ; # arbitrary value known to be larger than the menubar height } } } # testutils -- # # Takes care of exporting/importing/forgetting utility procs and any # associated variables from a specific test domain (functional area). # # More information is available in the file "testutils.GUIDE" # # Arguments: # subCmd : "export", "import" or "forget" # args : a sequence of domains that need to be imported/forgotten, # unused for "export" # proc testutils {subCmd args} { variable importedDomains variable importVars if {$subCmd ni [list export import forget]} { return -code error "invalid subCmd \"$subCmd\". Usage: [lindex [info level 0] 0] export|import|forget ?domain domain ...?" } set argc [llength $args] if {$subCmd eq "export"} { if {$argc != 0} { return -code error "invalid #args. Usage: [lindex [info level 0] 0] export" } # export all procs from the invoking domain namespace except "init" uplevel 1 { if {[info procs init] eq "init"} { set exports [info procs] namespace export {*}[lremove $exports [lsearch $exports "init"]] unset exports } else { namespace export * } } return } if {$argc < 1} { return -code error "invalid #args. Usage: [lindex [info level 0] 0] import|forget domain ?domain ...?" } # determine the requesting namespace set ns [uplevel 1 {namespace current}] # import/forget domains foreach domain $args { if {! [namespace exists ::tk::test::$domain]} { return -code error "testutils domain \"$domain\" doesn't exist" } switch -- $subCmd { import { if {[info exists importedDomains($ns)] && ($domain in $importedDomains($ns))} { return -code error "testutils domain \"$domain\" was already imported" } else { # import procs if {[catch { uplevel 1 [list namespace import ::tk::test::${domain}::*] } errMsg]} { # revert import of procs already done uplevel 1 [list namespace forget ::tk::test::${domain}::*] return -code error "import from testutils domain \"$domain\" failed: $errMsg" } # import associated namespace variables declared in the init proc if {"init" in [namespace inscope ::tk::test::$domain {info procs init}]} { if {[info exists importVars($ns,$domain)]} { # # Note [A1]: # If test files inadvertently leave behind a variable with the same name # as an upvar'ed namespace variable, its last value will serve as a new # initial value in case that the init proc declares that variable without # a value. Also, the result of "info exists varName" would be different # between test files. # # The next unset prevents such artefacts. See also note [A2] below. # uplevel 1 [list unset -nocomplain {*}$importVars($ns,$domain)] } ::tk::test::${domain}::init if {($ns ne "::") || (! [info exists importVars($ns,$domain)])} { # # Importing associated namespace variables into the global namespace where # tests are normally executing, needs to be done only once because an upvar # link cannot be removed from a namespace. For other requesting namespaces # we need to reckon with deletion and re-creation of the namespace in the # meantime. # if {[info exists importVars($ns,$domain)]} { set associatedVars $importVars($ns,$domain) } else { set associatedVars [namespace inscope ::tk::test::$domain {info vars}] } foreach varName $associatedVars { if {[catch { uplevel 1 [list upvar #0 ::tk::test::${domain}::$varName $varName] } errMsg]} { # revert imported procs and partial variable import uplevel 1 [list unset -nocomplain {*}$associatedVars] uplevel 1 [list namespace forget ::tk::test::${domain}::*] return -code error "import from testutils domain \"$domain\" failed: $errMsg" } } set importVars($ns,$domain) $associatedVars } } # register domain as imported lappend importedDomains($ns) $domain } } forget { if {(! [info exists importedDomains($ns)]) || ($domain ni $importedDomains($ns))} { return -code error "testutils domain \"$domain\" was not imported" } # remove imported utility procs from the namespace where tests are executing uplevel 1 [list namespace forget ::tk::test::${domain}::*] # # Some namespace variables are meant to persist across test files # in the entire Tk test suite (notably the variable ImageNames, # domain "image"). These variables are also not meant to be accessed # from, and imported into the namespace where tests are executing, # and they should not be cleaned up here. # if {[info exists importVars($ns,$domain)]} { # # Remove imported namespace variables. # # Note [A2]: # The upvar link in the namespace where tests are executing cannot be removed. # Without specific attention, this can cause surprising behaviour upon # re-initialization. See also note [A1] above. # uplevel 1 [list unset -nocomplain {*}$importVars($ns,$domain)] } set importedDomains($ns) [lremove $importedDomains($ns) [lsearch $importedDomains($ns) $domain]] } } } } testutils export } # Import generic utility procs into the global namespace (in which tests are # normally executing) as a standard policy. ::tk::test::generic::testutils import generic namespace eval ::tk::test::button { proc bogusTrace args { error "trace aborted" } testutils export } namespace eval ::tk::test::child { # childTkInterp -- # # Create a new Tk application in a child interpreter, with # a given name and class. # proc childTkInterp {name args} { set index [lsearch $args "-safe"] if {$index >= 0} { set safe 1 set options [lremove $args $index] } else { set safe 0 set options $args } if {[llength $options] ni {0 2}} { return -code error "invalid #args" } set cmdArgs [list -name $name] foreach {key value} $options { if {$key ne "-class"} { return -code error "invalid option \"$key\"" } lappend cmdArgs $key $value } if {$safe} { interp create -safe $name } else { interp create $name } $name eval [list set argv $cmdArgs] catch {eval [loadTkCommand] $name} } # childTkProcess -- # # Create a new Tk application in a child process, and enable it to # evaluate scripts on our behalf. # # Suggestion: replace with child interp or thread ? # proc childTkProcess {subcmd args} { variable fd switch -- $subcmd { create { if {[info exists fd] && [string length $fd]} { childTkProcess exit } set fd [open "|[list [::tcltest::interpreter] \ -geometry +0+0 -name tktest] $args" r+] puts $fd "puts foo; flush stdout" flush $fd if {[gets $fd data] < 0} { error "unexpected EOF from \"[::tcltest::interpreter]\"" } if {$data ne "foo"} { error "unexpected output from\ background process: \"$data\"" } puts $fd [loadTkCommand] flush $fd fileevent $fd readable [namespace code {childTkProcess read}] } eval { variable Data variable Done set script [lindex $args 0] set block 0 if {[llength $args] == 2} { set block [lindex $args 1] } if {$block} { fileevent $fd readable {} } puts $fd "[list catch $script msg]; update; puts \$msg;\ puts **DONE**; flush stdout" flush $fd set Data {} if {$block} { while {![eof $fd]} { set line [gets $fd] if {$line eq "**DONE**"} { break } append Data $line } } else { set Done 0 vwait [namespace which -variable Done] } return $Data } exit { # catch in case the child process has closed $fd catch {puts $fd exit} catch {close $fd} set fd "" } read { variable Data variable Done set x [gets $fd] if {[eof $fd]} { fileevent $fd readable {} set Done 1 } elseif {$x eq "**DONE**"} { set Done 1 } else { append Data $x } } } } testutils export } namespace eval ::tk::test::colors { # colorsFree -- # # Returns 1 if there appear to be free colormap entries in a window, 0 # otherwise. # # Arguments: # w : name of window in which to check. # red, green, blue : intensities to use in a trial color allocation # to see if there are colormap entries free. # proc colorsFree {w {red 31} {green 245} {blue 192}} { lassign [winfo rgb $w [format "#%02x%02x%02x" $red $green $blue]] r g b expr {($r/256 == $red) && ($g/256 == $green) && ($b/256 == $blue)} } # eatColors -- # # Creates a toplevel window and allocates enough colors in it to use up all # the slots in an 8-bit colormap. # # Arguments: # w : name of toplevel window to create. # proc eatColors {w} { catch {destroy $w} toplevel $w wm geom $w +0+0 canvas $w.c -width 400 -height 200 -bd 0 pack $w.c for {set y 0} {$y < 8} {incr y} { for {set x 0} {$x < 40} {incr x} { set color [format #%02x%02x%02x [expr {$x*6}] [expr {$y*30}] 0] $w.c create rectangle [expr {10*$x}] [expr {20*$y}] \ [expr {10*$x + 10}] [expr {20*$y + 20}] -outline {} \ -fill $color } } update } testutils export } namespace eval ::tk::test::dialog { # init -- # # This is a reserved proc that is part of the mechanism that the proc # testutils employs when making utility procs and associated namespace # variables available to test files. # # Test authors should define and initialize namespace variables here if # they need to be imported into the namespace in which tests are executing. # This proc must not be exported. # # For more information, see the documentation in the file "testutils.GUIDE" # proc init {} { variable dialogType [file rootname [file tail [info script]]] variable dialogIsNative [isNative $dialogType] variable testDialog variable testDialogFont } proc Click {button} { variable dialogType variable testDialog switch -- $dialogType { "fontchooser" { if {$button ni "ok cancel apply"} { return -code error "invalid button name \"$button\"" } $testDialog.$button invoke } "winDialog" { switch -exact -- $button { ok { set button 1 } cancel { set button 2 } } testwinevent $testDialog $button WM_LBUTTONDOWN 1 0x000a000b testwinevent $testDialog $button WM_LBUTTONUP 0 0x000a000b } default { return -code error "invalid dialog type \"$dialogType\"" } } } proc isNative {type} { switch -- $type { "choosedir" { set cmd ::tk_chooseDirectory } "clrpick" { set cmd ::tk_chooseColor } "filebox" { set cmd ::tk_getOpenFile } "msgbox" { set cmd ::tk_messageBox } "dialog" - "fontchooser" - "winDialog" { return "N/A" } default { return -code error "invalid dialog type \"$type\"" } } return [expr {[info procs $cmd] eq ""}] } proc PressButton {btn} { event generate $btn event generate $btn -x 5 -y 5 event generate $btn -x 5 -y 5 } proc SendButtonPress {parent btn buttonType} { variable dialogType switch -- $dialogType { "choosedir" { if {$parent eq "."} { set w .__tk_choosedir } else { set w $parent.__tk_choosedir } upvar ::tk::dialog::file::__tk_choosedir data } "clrpick" { set w .__tk__color upvar ::tk::dialog::color::[winfo name $w] data } "filebox" { if {$parent eq "."} { set w .__tk_filedialog } else { set w $parent.__tk_filedialog } upvar ::tk::dialog::file::__tk_filedialog data } "msgbox" { if {$parent eq "."} { set w .__tk__messagebox } else { set w $parent.__tk__messagebox } } default { return -code error "invalid dialog type \"$dialogType\"" } } if {$dialogType eq "msgbox"} { set button $w.$btn } else { set button $data($btn\Btn) } if {! [winfo ismapped $button]} { update } if {$buttonType eq "mouse"} { PressButton $button } else { event generate $w focus $w event generate $button event generate $w -keysym Return } } proc testDialog {stage {script ""}} { variable testDialogCmd variable testDialogResult variable testDialogFont variable iter_after variable testDialog; # On MS Windows, this variable is set at the C level # by SetTestDialog() in tkWinDialog.c switch -- $stage { launch { set iter_after 0 set testDialog {} if {$::tcl_platform(platform) eq "windows"} { variable testDialogClass "#32770" } after 1 $script } onDisplay { set testDialogCmd $script set testDialogResult {} set testDialogFont {} if {$::tcl_platform(platform) eq "windows"} { # Do not make the delay too short. The newer Vista dialogs take # time to come up. after 500 [list [namespace current]::testDialog onDisplay2] } else { testDialog onDisplay2 } vwait ::tk::test::dialog::testDialogResult return $testDialogResult } onDisplay2 { set doRepeat 0 if {$::tcl_platform(platform) eq "windows"} { # On Vista and later, using the new file dialogs we have to # find the window using its title as testDialog will not be # set at the C level. variable testDialogClass if {[catch {testfindwindow "" $testDialogClass} testDialog]} { set doRepeat 1 } } elseif {$testDialog eq ""} { set doRepeat 1 } if {$doRepeat} { if {[incr iter_after] > 30} { set testDialogResult ">30 iterations waiting for testDialog" return } after 150 [list ::tk::test::dialog::testDialog onDisplay2] return } set testDialogResult [uplevel #0 $testDialogCmd] } default { return -code error "invalid parameter \"$stage\"" } } } proc ToPressButton {parent btn} { variable dialogIsNative if {! $dialogIsNative} { after 100 SendButtonPress $parent $btn mouse } } testutils export } namespace eval ::tk::test::entry { # init -- # # This is a reserved proc that is part of the mechanism that the proc # testutils employs when making utility procs and associated namespace # variables available to test files. # # Test authors should define and initialize namespace variables here if # they need to be imported into the namespace in which tests are executing. # This proc must not be exported. # # For more information, see the documentation in the file "testutils.GUIDE" # proc init {} { variable textVar variable validationData } # Handler for variable trace on namespace variable textVar proc override args { variable textVar 12345 } # Procedures used by widget validation tests proc validateCommand1 {W d i P s S v V} { variable validationData [list $W $d $i $P $s $S $v $V] return 1 } proc validateCommand2 {W d i P s S v V} { variable validationData [list $W $d $i $P $s $S $v $V] variable textVar mydata return 1 } proc validateCommand3 {W d i P s S v V} { variable validationData [list $W $d $i $P $s $S $v $V] return 0 } proc validateCommand4 {W d i P s S v V} { variable validationData [list $W $d $i $P $s $S $v $V] .e delete 0 end; .e insert end dovaldata return 0 } testutils export } namespace eval ::tk::test::geometry { proc getsize {w} { update return "[winfo reqwidth $w] [winfo reqheight $w]" } testutils export } namespace eval ::tk::test::image { proc imageCleanup {} { variable ImageNames foreach img [image names] { if {$img ni $ImageNames} {image delete $img} } } proc imageFinish {} { variable ImageNames set imgs [lsearch -all -inline -glob -not [lsort [image names]] ::tk::icons::indicator*] if {$imgs ne $ImageNames} { return -code error "images remaining: [image names] != $ImageNames" } imageCleanup } proc imageInit {} { variable ImageNames if {![info exists ImageNames]} { set ImageNames [lsearch -all -inline -glob -not [lsort [image names]] ::tk::icons::indicator*] } imageCleanup if {[lsort [image names]] ne $ImageNames} { return -code error "IMAGE NAMES mismatch: [image names] != $ImageNames" } } proc imageNames {} { variable ImageNames set r {} foreach img [image names] { if {$img ni $ImageNames} {lappend r $img} } return $r } testutils export } namespace eval ::tk::test::scroll { # init -- # # This is a reserved proc that is part of the mechanism that the proc # testutils employs when making utility procs and associated namespace # variables available to test files. # # Test authors should define and initialize namespace variables here if # they need to be imported into the namespace in which tests are executing. # This proc must not be exported. # # For more information, see the documentation in the file "testutils.GUIDE" # proc init {} { variable scrollInfo {} } # Used as the scrolling command for widgets, set with "-[xy]scrollcommand". # It saves the scrolling information in a namespace variable "scrollInfo". proc setScrollInfo {args} { variable scrollInfo $args } testutils export } namespace eval ::tk::test::select { # init -- # # This is a reserved proc that is part of the mechanism that the proc # testutils employs when making utility procs and associated namespace # variables available to test files. # # Test authors should define and initialize namespace variables here if # they need to be imported into the namespace in which tests are executing. # This proc must not be exported. # # For more information, see the documentation in the file "testutils.GUIDE" # proc init {} { variable selValue {} selInfo {} variable abortCount variable pass } proc badHandler {path type offset count} { variable selInfo variable selValue selection handle -type $type $path {} lappend selInfo $path $type $offset $count set numBytes [expr {[string length $selValue] - $offset}] if {$numBytes <= 0} { return "" } string range $selValue $offset [expr {$numBytes+$offset}] } proc badHandler2 {path type offset count} { variable abortCount variable selInfo variable selValue incr abortCount -1 if {$abortCount == 0} { selection handle -type $type $path {} } lappend selInfo $path $type $offset $count set numBytes [expr {[string length $selValue] - $offset}] if {$numBytes <= 0} { return "" } string range $selValue $offset [expr {$numBytes+$offset}] } proc errHandler args { error "selection handler aborted" } proc errIncrHandler {type offset count} { variable selInfo variable selValue variable pass if {$offset == 4000} { if {$pass == 0} { # Just sizing the selection; don't do anything here. set pass 1 } else { # Fetching the selection; wait long enough to cause a timeout. after 6000 } } lappend selInfo $type $offset $count set numBytes [expr {[string length $selValue] - $offset}] if {$numBytes <= 0} { return "" } string range $selValue $offset [expr $numBytes+$offset] } proc handler {type offset count} { variable selInfo variable selValue lappend selInfo $type $offset $count set numBytes [expr {[string length $selValue] - $offset}] if {$numBytes <= 0} { return "" } string range $selValue $offset [expr $numBytes+$offset] } proc reallyBadHandler {path type offset count} { variable selInfo variable selValue variable pass if {$offset == 4000} { if {$pass == 0} { set pass 1 } else { selection handle -type $type $path {} } } lappend selInfo $path $type $offset $count set numBytes [expr {[string length $selValue] - $offset}] if {$numBytes <= 0} { return "" } string range $selValue $offset [expr {$numBytes+$offset}] } proc selectionSetup {{path .f1} {display {}}} { catch {destroy $path} if {$display eq ""} { frame $path } else { toplevel $path -screen $display wm geom $path +0+0 } selection own $path } testutils export } namespace eval ::tk::test::text { # init -- # # This is a reserved proc that is part of the mechanism that the proc # testutils employs when making utility procs and associated namespace # variables available to test files. # # Test authors should define and initialize namespace variables here if # they need to be imported into the namespace in which tests are executing. # This proc must not be exported. # # For more information, see the documentation in the file "testutils.GUIDE" # proc init {} { variable fixedFont {Courier -12} variable fixedWidth [font measure $fixedFont m] variable fixedHeight [font metrics $fixedFont -linespace] variable fixedAscent [font metrics $fixedFont -ascent] } # full border size of the text widget, i.e. first x or y coordinate inside the text widget # warning: -padx is supposed to be the same as -pady (same border size horizontally and # vertically around the widget) proc bo {{w .t}} { return [expr {[$w cget -borderwidth] + [$w cget -highlightthickness] + [$w cget -padx]}] } # x-coordinate of the first pixel of $n-th char (count starts at zero), left justified proc xchar {n {w .t}} { return [expr {[bo $w] + [xw $n]}] } # x-width of $n chars, fixed width font proc xw {n} { variable fixedWidth return [expr {$n * $fixedWidth}] } # y-coordinate of the first pixel of $l-th display line (count starts at 1) proc yline {l {w .t}} { variable fixedHeight return [expr {[bo $w] + ($l - 1) * $fixedHeight}] } testutils export } # EOF