# -*- tcl -*-
# (C) 2006 Andreas Kupries <[email protected]>
##
# ###
package require sak::test::shell
package require sak::registry
package require sak::animate
package require sak::color
# TODO: Rework this package to use the sak::feedback package
getpackage textutil::repeat textutil/repeat.tcl
getpackage fileutil fileutil/fileutil.tcl
getpackage struct::matrix struct/matrix.tcl
namespace eval ::sak::test::run {
namespace import ::textutil::repeat::blank
namespace import ::sak::color::*
}
# ###
proc ::sak::test::run {argv} {
variable run::valgrind
array set config {
valgrind 0 raw 0 shells {} stem {} log 0
}
while {[string match -* [set opt [lindex $argv 0]]]} {
switch -exact -- $opt {
-s - --shell {
set sh [lindex $argv 1]
if {![fileutil::test $sh efrx msg "Shell"]} {
sak::test::usage $msg
}
lappend config(shells) $sh
set argv [lrange $argv 2 end]
}
-g - --valgrind {
if {![llength $valgrind]} {
sak::test::usage valgrind not found in the PATH
}
incr config(valgrind)
set argv [lrange $argv 1 end]
}
-v {
set config(raw) 1
set argv [lrange $argv 1 end]
}
-l - --log {
set config(log) 1
set config(stem) [lindex $argv 1]
set argv [lrange $argv 2 end]
}
default {
sak::test::usage Unknown option "\"$opt\""
}
}
}
if {$config(log)} {set config(raw) 0}
if {![sak::util::checkModules argv]} return
run::Do config $argv
return
}
# ###
proc ::sak::test::run::Do {cv modules} {
upvar 1 $cv config
variable valgrind
variable araw $config(raw)
variable alog $config(log)
variable xttimes {}
# alog => !araw
set shells $config(shells)
if {![llength $shells]} {
catch {set shells [sak::test::shell::list]}
}
if {![llength $shells]} {
set shells [list [info nameofexecutable]]
}
if {$alog} {
variable logext [open $config(stem).log w]
variable logsum [open $config(stem).summary w]
variable logfai [open $config(stem).failures w]
variable logski [open $config(stem).skipped w]
variable lognon [open $config(stem).none w]
variable logerd [open $config(stem).errdetails w]
variable logfad [open $config(stem).faildetails w]
# Timings per testsuite (sec), average test timings (usec)
variable logtim [open $config(stem).timings w]
variable logtmt [open $config(stem).timetable w]
# Timings per test (usec)
variable logtti [open $config(stem).t-timings w]
variable logtmi [open $config(stem).t-timetable w]
} else {
variable logext stdout
}
# Preprocessing of module names and shell versions to allows
# better formatting of the progress output, i.e. vertically
# aligned columns
if {!$araw} {
variable maxml 0
variable maxvl 0
sak::animate::init
foreach m $modules {
= "M $m"
set l [string length $m]
if {$l > $maxml} {set maxml $l}
}
foreach sh $shells {
= "SH $sh"
set v [exec $sh << {puts [info patchlevel]; exit}]
set l [string length $v]
if {$l > $maxvl} {set maxvl $l}
}
=| "Starting ..."
}
set total 0
set pass 0
set fail 0
set skip 0
set err 0
foreach sh $shells {
foreach m $modules {
set cmd [Command config $m $sh]
sak::animate::init
if {$alog || $araw} {
puts $logext ============================================================
flush $logext
}
if {[catch {Close [Process [open |$cmd r+]]} msg]} {
incr err
=| "~~ [mag]ERR ${msg}[rst]"
if {$alog || $araw} {
puts $logext [mag]$msg[rst]
flush $logext
}
}
#sak::animate::last Ok
}
}
puts $logext "Passed [format %6d $pass] of [format %6d $total]"
puts $logext "Skipped [format %6d $skip] of [format %6d $total]"
if {$fail} {
puts $logext "Failed [red][format %6d $fail][rst] of [format %6d $total]"
} else {
puts $logext "Failed [format %6d $fail] of [format %6d $total]"
}
if {$err} {
puts $logext "#Errors [mag][format %6d $err][rst]"
} else {
puts $logext "#Errors [format %6d $err]"
}
flush $logext
=| "... Done"
if {$alog} {
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Timings per testsuite
=| "... Postprocessing per-testsuite timings ..."
variable xtimes
struct::matrix M
M add columns 6
M add row {Shell Module Testsuite Tests Seconds uSec/Test}
M add row {===== ====== ========= ===== ======= =========}
foreach item [lsort -decreasing -int -index 3 [lsort -dict -index 0 $xtimes]] {
foreach {k testnum delta score} $item break
M add row [linsert $k end $testnum $delta $score]
}
M add row {===== ====== ========= ===== ======= =========}
puts $logtmt "\nTiming Table..."
puts $logtmt [M format 2string]
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Timings per testcase.
=| "... Postprocessing per-test timings ..."
variable xttimes
struct::matrix MX
MX add columns 5
MX add row {Shell Module Testsuite Test uSec}
MX add row {===== ====== ========= ==== ====}
foreach item [lsort -index 1 -integer -decreasing [lsort -index 0 -dict $xttimes]] {
foreach {k usec} $item break
MX add row [linsert $k end $usec]
}
MX add row {===== ====== ========= ==== ====}
puts $logtmi "\nTiming Table..."
puts $logtmi [MX format 2string]
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
=| "... Postprocessing Done"
}
exit [expr {($err || $fail) ? 1 : 0}]
return
}
# ###
if {$::tcl_platform(platform) == "windows"} {
proc ::sak::test::run::Command {cv m sh} {
variable valgrind
upvar 1 $cv config
# Windows. Construction of the pipe to run a specific
# testsuite against a single shell. There is no valgrind to
# accomodate, and neither can we expect to have unix commands
# like 'echo' and 'cat' available. 'echo' we can go without. A
# 'cat' however is needed to merge stdout and stderr of the
# testsuite for processing here. We use an emuluation written
# in Tcl.
set catfile cat[pid].tcl
fileutil::writeFile $catfile {
catch {wm withdraw .}
while {![eof stdin]} {puts stdout [gets stdin]}
exit
}
set cmd ""
lappend cmd $sh
lappend cmd [Driver] -modules [list $m]
lappend cmd |& $sh $catfile
#puts <<$cmd>>
return $cmd
}
proc ::sak::test::run::Close {pipe} {
close $pipe
file delete cat[pid].tcl
return
}
} else {
proc ::sak::test::run::Command {cv m sh} {
variable valgrind
upvar 1 $cv config
# Unix. Construction of the pipe to run a specific testsuite
# against a single shell. The command is constructed to work
# when using valgrind, and works when not using it as well.
set script {}
lappend script [list set argv [list -modules [list $m]]]
lappend script {set argc 2}
lappend script [list source [Driver]]
lappend script exit
set cmd ""
lappend cmd echo [join $script \n]
lappend cmd |
if {$config(valgrind)} {
foreach e $valgrind {lappend cmd $e}
if {$config(valgrind) > 1} {
lappend cmd --num-callers=8
lappend cmd --leak-resolution=high
lappend cmd -v --leak-check=yes
lappend cmd --show-reachable=yes
}
}
lappend cmd $sh
#lappend cmd >@ stdout 2>@ stderr
lappend cmd |& cat
#puts <<$cmd>>
return $cmd
}
proc ::sak::test::run::Close {pipe} {
close $pipe
return
}
}
# ###
proc ::sak::test::run::Process {pipe} {
variable araw
variable alog
variable logext
while {1} {
if {[eof $pipe]} break
if {[gets $pipe line] < 0} break
if {$alog || $araw} {puts $logext $line ; flush $logext}
set rline $line
set line [string trim $line]
if {[string equal $line ""]} continue
Host; Platform
Cwd; Shell
Tcl
Start; End ; StartFile ; EndFile
Module; Testsuite
NoTestsuite
Support;Testing;Other
Summary
CaptureFailureSync ; # xcollect 1 => 2
CaptureFailureCollectBody ; # xcollect 2 => 3 => 5
CaptureFailureCollectActual ; # xcollect 3 => 4
CaptureFailureCollectExpected ; # xcollect 4 => 0
CaptureFailureCollectError ; # xcollect 5 => 0
CaptureStackStart
CaptureStack
TestStart
TestTook
TestSkipped
TestPassed
TestFailed ; # xcollect => 1
SetupError
Aborted
AbortCause
Match||Skip||Sourced
# Unknown lines are printed
if {!$araw} {puts !$line}
}
return $pipe
}
# ###
proc ::sak::test::run::Driver {} {
variable base
return [file join $base all.tcl]
}
# ###
proc ::sak::test::run::Host {} {
upvar 1 line line ; variable xhost
if {![regexp "^@@ Host (.*)$" $line -> xhost]} return
# += $xhost
set xhost [list Tests Results $xhost]
#sak::registry::local set $xhost
return -code continue
}
proc ::sak::test::run::Platform {} {
upvar 1 line line ; variable xplatform
if {![regexp "^@@ Platform (.*)$" $line -> xplatform]} return
# += ($xplatform)
variable xhost
#sak::registry::local set $xhost Platform $xplatform
return -code continue
}
proc ::sak::test::run::Cwd {} {
upvar 1 line line ; variable xcwd
if {![regexp "^@@ CWD (.*)$" $line -> xcwd]} return
variable xhost
set xcwd [linsert $xhost end $xcwd]
#sak::registry::local set $xcwd
return -code continue
}
proc ::sak::test::run::Shell {} {
upvar 1 line line ; variable xshell
if {![regexp "^@@ Shell (.*)$" $line -> xshell]} return
# += [file tail $xshell]
variable xcwd
set xshell [linsert $xcwd end $xshell]
#sak::registry::local set $xshell
return -code continue
}
proc ::sak::test::run::Tcl {} {
upvar 1 line line ; variable xtcl
if {![regexp "^@@ Tcl (.*)$" $line -> xtcl]} return
variable xshell
variable maxvl
+= \[$xtcl\][blank [expr {$maxvl - [string length $xtcl]}]]
#sak::registry::local set $xshell Tcl $xtcl
return -code continue
}
proc ::sak::test::run::Match||Skip||Sourced {} {
upvar 1 line line
if {[string match "@@ Skip*" $line]} {return -code continue}
if {[string match "@@ Match*" $line]} {return -code continue}
if {[string match "Sourced * Test Files." $line]} {return -code continue}
if {[string match "Files with failing tests*" $line]} {return -code continue}
if {[string match "Number of tests skipped*" $line]} {return -code continue}
if {[string match "\[0-9\]*" $line]} {return -code continue}
return
}
proc ::sak::test::run::Start {} {
upvar 1 line line
if {![regexp "^@@ Start (.*)$" $line -> start]} return
variable xshell
#sak::registry::local set $xshell Start $start
return -code continue
}
proc ::sak::test::run::End {} {
upvar 1 line line
if {![regexp "^@@ End (.*)$" $line -> end]} return
variable xshell
#sak::registry::local set $xshell End $end
return -code continue
}
proc ::sak::test::run::StartFile {} {
upvar 1 line line
if {![regexp "^@@ StartFile (.*)$" $line -> start]} return
variable xstartfile $start
variable xtestnum 0
#sak::registry::local set $xshell Start $start
return -code continue
}
proc ::sak::test::run::EndFile {} {
upvar 1 line line
if {![regexp "^@@ EndFile (.*)$" $line -> end]} return
variable xfile
variable xstartfile
variable xtimes
variable xtestnum
variable xduration
set k [lreplace $xfile 0 3]
set k [lreplace $k 2 2 [file tail [lindex $k 2]]]
set delta [expr {$end - $xstartfile}]
incr xduration $delta
if {$xtestnum == 0} {
set score $delta
} else {
# average number of microseconds per test.
set score [expr {int(($delta/double($xtestnum))*1000000)}]
#set score [expr {$delta/double($xtestnum)}]
}
lappend xtimes [list $k $xtestnum $delta $score]
variable alog
if {$alog} {
variable logtim
puts $logtim [linsert [linsert $k end $xtestnum $delta $score] 0 TIME]
}
#sak::registry::local set $xshell End $end
return -code continue
}
proc ::sak::test::run::Module {} {
upvar 1 line line ; variable xmodule
if {![regexp "^@@ Module (.*)$" $line -> xmodule]} return
variable xshell
variable xstatus ok
variable maxml
variable xduration 0
+= ${xmodule}[blank [expr {$maxml - [string length $xmodule]}]]
set xmodule [linsert $xshell end $xmodule]
#sak::registry::local set $xmodule
return -code continue
}
proc ::sak::test::run::Testsuite {} {
upvar 1 line line ; variable xfile
if {![regexp "^@@ Testsuite (.*)$" $line -> xfile]} return
= <[file tail $xfile]>
variable xmodule
set xfile [linsert $xmodule end $xfile]
#sak::registry::local set $xfile Aborted 0
return -code continue
}
proc ::sak::test::run::NoTestsuite {} {
upvar 1 line line
if {![string match "Error: No test files remain after*" $line]} return
variable xstatus none
= {No tests}
return -code continue
}
proc ::sak::test::run::Support {} {
upvar 1 line line
if {![regexp "^- (.*)$" $line -> package]} return
#= "S $package"
foreach {pn pv} $package break
variable xfile
#sak::registry::local set [linsert $xfile end Support] $pn $pv
return -code continue
}
proc ::sak::test::run::Testing {} {
upvar 1 line line
if {![regexp "^\\* (.*)$" $line -> package]} return
#= "T $package"
foreach {pn pv} $package break
variable xfile
#sak::registry::local set [linsert $xfile end Testing] $pn $pv
return -code continue
}
proc ::sak::test::run::Other {} {
upvar 1 line line
if {![string match ">*" $line]} return
return -code continue
}
proc ::sak::test::run::Summary {} {
upvar 1 line line
if {![regexp "^all\\.tcl:(.*)$" $line -> line]} return
variable xmodule
variable xstatus
variable xvstatus
foreach {_ t _ p _ s _ f} [split [string trim $line]] break
#sak::registry::local set $xmodule Total $t ; set t [format %5d $t]
#sak::registry::local set $xmodule Passed $p ; set p [format %5d $p]
#sak::registry::local set $xmodule Skipped $s ; set s [format %5d $s]
#sak::registry::local set $xmodule Failed $f ; set f [format %5d $f]
upvar 2 total _total ; incr _total $t
upvar 2 pass _pass ; incr _pass $p
upvar 2 skip _skip ; incr _skip $s
upvar 2 fail _fail ; incr _fail $f
upvar 2 err _err
set t [format %5d $t]
set p [format %5d $p]
set s [format %5d $s]
set f [format %5d $f]
if {$xstatus == "ok" && $t == 0} {
set xstatus none
set spent ""
} else {
# Time spent on all the files in the module.
variable xduration
#set sec $xduration
#set min [expr {$sec / 60}]
#set sec [expr {$sec % 60}]
#set hor [expr {$min / 60}]
#set min [expr {$min % 60}]
#set spent " :[format %02d $hor]h[format %02d $min]m[format %02d $sec]s"
set spent " @${xduration}s"
}
set st $xvstatus($xstatus)
if {$xstatus == "ok"} {
# Quick return for ok suite.
=| "~~ $st T $t P $p S $s F $f$spent"
return -code continue
}
# Clean out progress display using a non-highlighted
# string. Prevents the char count from being off. This is
# followed by construction and display of the highlighted version.
= " $st T $t P $p S $s F $f$spent"
switch -exact -- $xstatus {
none {=| "~~ [yel]$st T $t[rst] P $p S $s F $f"}
aborted {=| "~~ [whi]$st[rst] T $t P $p S $s F $f$spent"}
error {=| "~~ [mag]$st[rst] T $t P $p S $s F $f$spent" ; incr _err }
fail {=| "~~ [red]$st[rst] T $t P $p S $s [red]F $f[rst]$spent"}
}
return -code continue
}
proc ::sak::test::run::TestStart {} {
upvar 1 line line
if {![string match {---- * start} $line]} return
set testname [string range $line 5 end-6]
= "---- $testname"
variable xfile
variable xtesttime -1
variable xtest [linsert $xfile end $testname]
variable xtestnum
incr xtestnum
return -code continue
}
proc ::sak::test::run::TestTook {} {
upvar 1 line line
if {![string match {++++ * took *} $line]} return
# Dynamic search for the marker because the name of the test may
# contain spaces, causing the field position to vary.
set pos [lsearch -exact $line took]
incr pos
set usec [lindex $line $pos]
variable xtesttime $usec
return -code continue
}
proc ::sak::test::run::TestSkipped {} {
upvar 1 line line
if {![string match {++++ * SKIPPED:*} $line]} return
regexp {^[^ ]* (.*)SKIPPED:.*$} $line -> testname
set testname [string trim $testname]
variable xtest
= "SKIP $testname"
if {$xtest == {}} {
variable xfile
set xtest [linsert $xfile end $testname]
}
#sak::registry::local set $xtest Status Skip
set xtest {}
return -code continue
}
proc ::sak::test::run::TestPassed {} {
upvar 1 line line
if {![string match {++++ * PASSED} $line]} return
set testname [string range $line 5 end-7]
variable xtesttime
variable xtest
if {$xtesttime < 0} { set xtesttime "" }
= [string trimright "PASS $testname $xtesttime"]
if {$xtest == {}} {
variable xfile
set xtest [linsert $xfile end $testname]
}
#sak::registry::local set $xtest Status Pass
variable alog
if {$alog && ($xtesttime ne {})} {
variable xttimes
variable logtti
set k [lreplace $xtest 0 3]
set k [lreplace $k 2 2 [file tail [lindex $k 2]]]
# k = shell module testfile testname
puts $logtti [linsert [linsert $k 0 TIME] end $xtesttime]
lappend xttimes [list $k $xtesttime]
}
set xtest {}
return -code continue
}
proc ::sak::test::run::TestFailed {} {
upvar 1 line line
if {![string match {==== * FAILED} $line]} return
set testname [lindex [split [string range $line 5 end-7]] 0]
= "FAIL $testname"
variable xtest
if {$xtest == {}} {
variable xfile
set xtest [linsert $xfile end $testname]
}
#sak::registry::local set $xtest Status Fail
## CAPTURE INIT
variable xcollect 1
variable xbody ""
variable xactual ""
variable xexpected ""
variable xstatus fail
# Ignore failed status if we already have it, or an error
# status. The latter is more important to show. We do override
# status 'aborted'.
if {$xstatus == "ok"} {set xstatus fail}
if {$xstatus == "aborted"} {set xstatus fail}
return -code continue
}
proc ::sak::test::run::CaptureFailureSync {} {
variable xcollect
if {$xcollect != 1} return
upvar 1 line line
if {![string match {==== Contents*} $line]} return
set xcollect 2
return -code continue
}
proc ::sak::test::run::CaptureFailureCollectBody {} {
variable xcollect
if {$xcollect != 2} return
upvar 1 rline line
variable xbody
if {[string match {---- Result was*} $line]} {
set xcollect 3
return -code continue
} elseif {[string match {---- Test generated error*} $line]} {
set xcollect 5
return -code continue
}
variable xbody
append xbody $line \n
return -code continue
}
proc ::sak::test::run::CaptureFailureCollectActual {} {
variable xcollect
if {$xcollect != 3} return
upvar 1 rline line
if {![string match {---- Result should*} $line]} {
variable xactual
append xactual $line \n
} else {
set xcollect 4
}
return -code continue
}
proc ::sak::test::run::CaptureFailureCollectExpected {} {
variable xcollect
if {$xcollect != 4} return
upvar 1 rline line
if {![string match {==== *} $line]} {
variable xexpected
append xexpected $line \n
} else {
variable alog
if {$alog} {
variable logfad
variable xtest
variable xbody
variable xactual
variable xexpected
puts $logfad "==== [lrange $xtest end-1 end] FAILED ========="
puts $logfad "==== Contents of test case:\n"
puts $logfad $xbody
puts $logfad "---- Result was:"
puts $logfad [string range $xactual 0 end-1]
puts $logfad "---- Result should have been:"
puts $logfad [string range $xexpected 0 end-1]
puts $logfad "==== [lrange $xtest end-1 end] ====\n\n"
flush $logfad
}
set xcollect 0
#sak::registry::local set $xtest Body $xbody
#sak::registry::local set $xtest Actual $xactual
#sak::registry::local set $xtest Expected $xexpected
set xtest {}
}
return -code continue
}
proc ::sak::test::run::CaptureFailureCollectError {} {
variable xcollect
if {$xcollect != 5} return
upvar 1 rline line
variable xbody
if {[string match {---- errorCode*} $line]} {
set xcollect 4
return -code continue
}
variable xactual
append xactual $line \n
return -code continue
}
proc ::sak::test::run::Aborted {} {
upvar 1 line line
if {![string match {Aborting the tests found *} $line]} return
variable xfile
variable xstatus
# Ignore aborted status if we already have it, or some other error
# status (like error, or fail). These are more important to show.
if {$xstatus == "ok"} {set xstatus aborted}
= Aborted
#sak::registry::local set $xfile Aborted {}
return -code continue
}
proc ::sak::test::run::AbortCause {} {
upvar 1 line line
if {
![string match {Requiring *} $line] &&
![string match {Error in *} $line]
} return ; # {}
variable xfile
= $line
#sak::registry::local set $xfile Aborted $line
return -code continue
}
proc ::sak::test::run::CaptureStackStart {} {
upvar 1 line line
if {![string match {@+*} $line]} return
variable xstackcollect 1
variable xstack {}
variable xstatus error
= {Error, capturing stacktrace}
return -code continue
}
proc ::sak::test::run::CaptureStack {} {
variable xstackcollect
if {!$xstackcollect} return
upvar 1 line line
variable xstack
if {![string match {@-*} $line]} {
append xstack [string range $line 2 end] \n
} else {
set xstackcollect 0
variable xfile
variable alog
#sak::registry::local set $xfile Stacktrace $xstack
if {$alog} {
variable logerd
puts $logerd "[lindex $xfile end] StackTrace"
puts $logerd "========================================"
puts $logerd $xstack
puts $logerd "========================================\n\n"
flush $logerd
}
}
return -code continue
}
proc ::sak::test::run::SetupError {} {
upvar 1 line line
if {![string match {SETUP Error*} $line]} return
variable xstatus error
= {Setup error}
return -code continue
}
# ###
proc ::sak::test::run::+= {string} {
variable araw
if {$araw} return
variable aprefix
append aprefix " " $string
sak::animate::next $aprefix
return
}
proc ::sak::test::run::= {string} {
variable araw
if {$araw} return
variable aprefix
sak::animate::next "$aprefix $string"
return
}
proc ::sak::test::run::=| {string} {
variable araw
if {$araw} return
variable aprefix
sak::animate::last "$aprefix $string"
variable alog
if {$alog} {
variable logsum
variable logfai
variable logski
variable lognon
variable xstatus
puts $logsum "$aprefix $string" ; flush $logsum
switch -exact -- $xstatus {
error -
fail {puts $logfai "$aprefix $string" ; flush $logfai}
none {puts $lognon "$aprefix $string" ; flush $lognon}
aborted {puts $logski "$aprefix $string" ; flush $logski}
}
}
set aprefix ""
return
}
# ###
namespace eval ::sak::test::run {
variable base [file join $::distribution support devel]
variable valgrind [auto_execok valgrind]
# State of test processing.
variable xstackcollect 0
variable xstack {}
variable xcollect 0
variable xbody {}
variable xactual {}
variable xexpected {}
variable xhost {}
variable xplatform {}
variable xcwd {}
variable xshell {}
variable xmodule {}
variable xfile {}
variable xtest {}
variable xstartfile {}
variable xtimes {}
variable xstatus ok
# Animation prefix of test processing, and flag controlling the
# nature of logging (raw vs animation).
variable aprefix {}
variable araw 0
# Max length of module names and patchlevel information.
variable maxml 0
variable maxvl 0
# Map from internal stati to the displayed human readable
# strings. This includes the trailing whitespace needed for
# vertical alignment.
variable xvstatus
array set xvstatus {
ok { }
none {None }
aborted {Skip }
error {ERR }
fail {FAILS}
}
}
##
# ###
package provide sak::test::run 1.0
if 0 {
# Bad valgrind, ok no valgrind
if {$config(valgrind)} {
foreach e $valgrind {lappend cmd $e}
lappend cmd --num-callers=8
lappend cmd --leak-resolution=high
lappend cmd -v --leak-check=yes
lappend cmd --show-reachable=yes
}
lappend cmd $sh
lappend cmd [Driver] -modules $modules
}