#This script is using xml to define testcases and testsuite. With this, it is very easier to create new testcases, without need to update the script framework;
#Testunit would be the basic unit in the xml, which corresponding to a proc in tcl script, as the function block, such as make connection, iperf traffic test, set/get MIB etc;
#A testcases.xml is a predefined testcase set, which only need to be updated when a new test scenario is needed;
#Each testcase contains one or more testunit, plus setting and expect result as attr;
#Testsuite xml is user defined collection of testcases and testunits;
#Each xml must have one and only one root tag, and struct like this:
# <root_tag attr1=xxx attr2=yyy ...>
# text
# <sub_tag1 sub_tag1_attr1 ...>
# <sub_tag2 sub_tag2_attr1 ...>
#In topologic, each tag is a node, attr is a list, and sub tags are sub nodes;
#http://wiki.tcl.tk/1740 convert content to xml format, default tag is 'document'
proc asXML {content {tag document}} {
set XML_MAP {
< <
> >
& &
\" "
' '
}
return <$tag>[string map $XML_MAP $content]</$tag>
}
#http://wiki.tcl.tk/3919 A little XML parser
#convert xml to: tag {list: attr} {list: #text {_text_}, subTag1, subTag2 ...}
proc xml2list xml {
# trim only deal with the begin and end of the whole xml
#puts "dbg trim: \[[string trim $xml " \n\t<>"]\]"
regsub -all {>\s*<} [string trim $xml " \n\t<>"] "\} \{" xml
# above regsub replace '>' and '<' with brace if no text between them. It also remove \r \n between '>' and '<'
#puts "dbg after regsub: \[$xml\]"
set xml [string map {> "\} \{#text \{" < "\}\} \{"} $xml]
#puts "dbg after map: \[$xml\]"
set res "" ;# string to collect the result
set stack {} ;# track open tags
set rest {}
foreach item "{$xml}" {
#puts "dbg item => $item"
switch -regexp -- $item {
^# {append res "{$item} " ; #text item, or "{#text [lrange $item 1 end]} ", origin post not work as it said: "{[lrange $item 0 end]} "}
^/ {
regexp {/(.+)} $item -> tagname ;# end tag
set expected [lindex $stack end]
if {$tagname!=$expected} {error "$item != $expected"}
set stack [lrange $stack 0 end-1]
append res "\}\} "
}
/$ { # singleton - start and end in one <> group
regexp {([^ ]+)( (.+))?/$} $item -> tagname - rest
#set rest [lrange [string map {= " "} $rest] 0 end] <= not work if attribute like: attr1="var=x"
regsub -all {=\s*"} $rest " \"" rest
regsub -all {=\s*'} $rest " '" rest
set rest [lrange $rest 0 end]
#with above two lines, attribute has to be single/double quoted
append res "{$tagname [list $rest] {}} "
}
default {
set tagname [lindex $item 0] ;# start tag
#set rest [lrange [string map {= " "} $item] 1 end] <= same as above, not work if attribute contains equal
regsub -all {=\s*"} $item " \"" item
regsub -all {=\s*'} $item " '" item
set rest [lrange $item 1 end]
#with above two lines, attribute value has to be single/double quoted, but the value should not contain such as ="
lappend stack $tagname
append res "\{$tagname [list $rest] \{"
}
}
if {[llength $rest]%2} {error "att's not paired: $rest"}
}
if [llength $stack] {error "unresolved: $stack"}
string map {"\} \}" "\}\}"} [lindex $res 0]
}
proc xputs {msg {log 0}} {
variable xputs_level
set indent ""
if {[info exists xputs_level] && [regexp {^\d+$} $xputs_level]} {set indent [string repeat { } $xputs_level]}
if {$log ne "0"} {send_user "$indent$msg\n"
} else {puts "$indent$msg"}
}
proc print_tree {lxml level} {
#puts "level = $level"
puts "[string repeat { } $level]tag = [lindex $lxml 0]"
puts "[string repeat { } $level]att = [lindex $lxml 1]"
incr level
foreach sitem [lindex $lxml 2] {
print_tree $sitem $level
}
}
#testcases.xml is xml file contains pre-defined testcases. This xml file would be processed to generate an array of testcases definition, no execution here
#testcase name is the index of the array, such as 'TC-SCAN-3-1', and each element of the array defines how to run the testcase, include setting, expect result, and testunit needs to be executed
#the analyze result will be set in variable testcases which is an array;
#Each testcase must have at least one TU, and setting/exp_res passed to the TU/TUs as arguments
#The test xml such as a Test Suite may have one or more testcases. All testcases within one xml node shares all settings, such as repeat counter, timeout setting
#Within one xml node, may have multiple setting, the later setting will be appended, override old value, but no reset. Setting won't be passed to sibling nodes
#Setting would be passed to direct child as reference, where a setting/expect node will update them, and a TC/TU node will take it as input args
#TODO: Currently setting won't be passed cross a node. Setting inherit more than parent? Setting per testcase? Good side: avoid pollution. Bad: cannot share
proc process_testcases_xml {tc_xml_file} {
variable testcases
variable tc_subs ;#for subs defined in testcases_xml
set fp [open $tc_xml_file]
set testcases_xml [read $fp]
close $fp
#puts "Original xml:\n$testcases_xml"
set l_tcs_xml [xml2list [regsub -all {(^|\n)\s*#[^\n]*\n} $testcases_xml {\1}]] ;#remove comment and covert xml to list
set tc_sub_list {}
#puts "tcs xml list: $l_tcs_xml"
#print_tree $l_tcs_xml 0
set ltcs [lindex $l_tcs_xml 2]
foreach tc $ltcs {
#puts "=>$tc"
#=>testcase {name TC-SCAN-3-1} {{setting {Operation Link, Chan 1, Mask 1, Repeat 4} {}} {expect {result success} {}} {run {name TU_scan_test} {}}}
if {[lindex $tc 0] eq "sub"} {
set tc_subs([lindex [lindex $tc 1] 1]) $tc
} else {set testcases([lindex [lindex $tc 1] 1]) [lindex $tc 2]}
}
#puts "\n _dbg_: dump tc_subs:" ; foreach {key value} [array get tc_subs] {puts " $key => $value"}
}
proc get_sub_xlst {lxml sub_name} {
set found false
if {[lindex $lxml 0] eq "sub"} {
array set attrs [lindex $lxml 1]
if {[info exists attrs(name)] && $attrs(name) eq $sub_name} {return $lxml}
}
foreach sitem [lindex $lxml 2] {
set found [get_sub_xlst $sitem $sub_name]
if {$found ne "false"} {return $found}
}
return $found
}
#xlst is a three elments list of a xml node: node_tag, list of attrs, list of sub_node; param level is for debug purpose
#p_setting and p_exp_res is parent setting and exp_res, for current node and current node's sibling nodes
#No inherit of setting and exp_res as that would be messy, error prone and complex. So here allow to pass optional setting as attr of TC.
#Note: there is two ways to run a TC testcase, either use 'run TC-xxx' to start a predefined TC, or have a tag's name started with "TC-"
#Add global AbortTC to abort all testcases whenever needed. Not abort TU as user may still want to do TU_TEST to dump data
set AbortTC no
proc process_xmlnode {xlst level {p_setting ""} {p_exp_res ""}} {
variable testcases
variable tc_subs
variable xputs_level
variable full_tree ;#for processing 'use'
set node [lindex $xlst 0]
set attrs [lindex $xlst 1]
#index 1 in xlst is key-value paired list and 1st pair should be a testunit or testcase name like TU-SETUP-HOSTS or TC-TRAFFIC-10
#other attr could be optional setting of the testcase, or override the TC setting, may also set like expected result
#convert attrs as array as it is key-value pairs
set subnodes [lindex $xlst 2]
set xputs_level $level
upvar $p_setting cur_setting $p_exp_res cur_exp_res
#setting and exp_res for sub nodes
set s_setting ""
set s_exp_res ""
#inherit parent setting/exp_res: 1st, it doesn't work; 2nd, it is a bad approach as explained above.
#set s_setting $p_setting
#set s_exp_res $p_exp_res
#xputs "Level $level node is <$node> with attr list: ($attrs)"
#TODO: decide what to do with the $attrs
if {$node eq "setting"} {
#put setting in upvar variable as it is for parent node, and only need to perform setting in one shot(TU-SETUP-HOSTS?) to make it more effient
set cur_setting [dict merge $cur_setting $attrs]
if {$::LOG_LEVEL>2} {xputs " A setting: $attrs"}
} elseif {$node eq "expect"} {
#put expect to an upvar variable as it is for parent node and it will be checked in testunit of the parent node
set cur_exp_res [dict merge $cur_exp_res $attrs]
if {$::LOG_LEVEL>2} {xputs " An expect: $attrs"}
} elseif {$node eq "exec"} {
#print a message, eval tcl; be careful as support is limited, due to nature of xml and tcl, as quote, slash and many special character will cause issue
#Note $attrs is treated as dict, so duplicate key in $attrs will get lost
#Also, for eval, set variable without uplevel will get lost as this is a recursive call of process_xmlnode
#For exec an external/system command, too many hassle: not sure the cwd, nowhere capture the output; Use eval if needed like this: <exec eval="puts [exec date]" />
if {$::LOG_LEVEL>2} {xputs " An exec request: $attrs"}
dict for {op args} $attrs {
switch -exact -- $op {
"echo" {puts $args}
"eval" {eval $args}
default {puts stderr "Unsupported operation: $op"}
}
}
} elseif {$node eq "run"} {
array set run_setting $attrs
if {[info exists run_setting(name)]} {
if {[regexp {^TC} $run_setting(name)]} {
variable tc_results
variable cur_tc_result
variable tc_idx
variable TC_name
variable f_csv
set TC_name $run_setting(name)
set cur_tc_result ""
#setting for testcase are ignored for now. Should be repeat number, timeout etc.
#upvar $setting cur_setting
puts ""
xputs " <Run testcase $TC_name" 1
if {[info exists f_csv]} {puts $f_csv "Run testcase $TC_name"}
if {$::AbortTC} {
set cur_tc_result "Aborted"
xputs " $cur_tc_result" 1
} else {
#pick the optional setting from TC attr
set s_setting [dict remove $attrs name]
foreach subnode $testcases($TC_name) {process_xmlnode $subnode [expr $level+1] s_setting s_exp_res}
}
dict set tc_results $TC_name $cur_tc_result
incr tc_idx
} elseif {[regexp {^TU} $run_setting(name)]} {
if {$::LOG_LEVEL>1} {xputs " <Run testunit $run_setting(name)" 1}
if {[catch {_$run_setting(name) $cur_setting $cur_exp_res} ret_code]} {
send_user "\n\n**** Exception while running $run_setting(name) ****\n$ret_code\n**** Full error info:\n$::errorInfo\n****\n"
#log_stacktrace ;# too many as this is inside recursive call
}
}
} else {xputs "\t## no-name run with attrs: [array get attrs]"}
} elseif {$node eq "use"} { ;#no attrs, neither sub-nodes expected with use
array set use_setting $attrs
if {[info exists use_setting(name)]} {
set sub_name $use_setting(name)
set use_xlst [get_sub_xlst $full_tree $sub_name] ;#search instead of create sub list on fly, so the sub can be declared after the place invoke it
if {$use_xlst eq "false" && [info exists tc_subs($sub_name)]} {set use_xlst $tc_subs($sub_name)}
if {$use_xlst eq "false"} {
xputs "\t## Not found $sub_name for use. Ignored!"
puts "\n _dbg_: existing tc_subs:" ; foreach {key value} [array get tc_subs] {puts " $key => $value"}
} else {
#puts "*process use: $use_xlst, s=$cur_setting, e=$cur_exp_res"
#process_xmlnode $use_xlst $level cur_setting cur_exp_res
#used node is a container, will ignore the attrs, and directly process subnodes of it
foreach subnode [lindex $use_xlst 2] {process_xmlnode $subnode $level cur_setting cur_exp_res}
}
} else {xputs "\t## Missing tag for use, useless attrs: [array get attrs]"}
} elseif {$node eq "sub"} { ;#sub block which will be invoked by others. so no processing here. Will search the list when needed
} else {
variable TC_name
variable tc_results
variable cur_tc_result
variable tc_idx
set is_TC no
if {[regexp -- {(TC-\S+)} $node -> TC_name]} {
send_user "\n\n<$TC_name>\n"
set is_TC yes
set cur_tc_result ""
}
#only node other than 'setting', 'expect' and 'run' may have sub-nodes
foreach subnode $subnodes {process_xmlnode $subnode [expr $level+1] s_setting s_exp_res}
if {$is_TC} {
dict set tc_results $TC_name $cur_tc_result
incr tc_idx
}
}
}
proc process_test_xml {xml_file {tc_xml_file ""}} {
variable testcases
variable full_tree
variable f_csv
if {[info exists f_csv]} {set create_local_mon_log no
} else {
set create_local_mon_log yes
set f_csv [open monitor.log w]
}
#first, process predefined testcases in $tc_xml_file
if {$tc_xml_file eq ""} {
if {[regexp {([^@]+)@([^@]+)} $xml_file -> xml_file tc_xml_file]==0} {set tc_xml_file $::XML_ROOT_DIR/testcases.xml}
}
process_testcases_xml $tc_xml_file
#foreach {tc_name tc_setting} [array get testcases] {puts "$tc_name => $tc_setting"}
#start processing xml_file
set fp [open $xml_file]
set test_xml [read $fp]
close $fp
#below regsub is for filtering out comment line in XML which is start with '#' as the 1st non-space character of a line, which is not conforming to the XML standard. May be removed in future
regsub -all {(^|\n)\s*#[^\n]*\n} $test_xml {\1} test_xml
set full_tree [xml2list $test_xml]
#print_tree $full_tree 0
variable tc_results
variable tc_idx 0
process_xmlnode $full_tree 0
if {$create_local_mon_log} {close $f_csv}
}
Saturday, April 11, 2020
Tcl parsing xml test description file
at 8:34 PM
Subscribe to:
Post Comments (Atom)
0 Comments:
Post a Comment