source: branches/uiuc_vtk_viewers/tester/scripts/test.tcl @ 5084

Last change on this file since 5084 was 3177, checked in by mmc, 12 years ago

Updated all of the copyright notices to reference the transfer to
the new HUBzero Foundation, LLC.

File size: 24.7 KB
Line 
1# ----------------------------------------------------------------------
2#  COMPONENT: test - run a test and query the results
3#
4#  Encapsulates the testing logic, to keep it isolated from the rest of
5#  the tester GUI.  Constructor requires the location of the tool.xml
6#  for the new version, and the test xml file containing the golden set
7#  of results.
8# ======================================================================
9#  AUTHOR:  Ben Rafferty, Purdue University
10#  Copyright (c) 2004-2012  HUBzero Foundation, LLC
11#
12#  See the file "license.terms" for information on usage and
13#  redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES.
14# ======================================================================
15
16namespace eval Rappture::Tester { #forward declaration }
17
18itcl::class Rappture::Tester::Test {
19    public variable notifycommand ""
20
21    constructor {tool testxml args} { #defined later }
22    destructor { #defined later }
23
24    public method getResult {}
25    public method getTestInfo {args}
26    public method getRunInfo {args}
27    public method getDiffs {args}
28
29    public method getTestobj {}
30    public method getTestxml {}
31
32    public method run {args}
33    public method abort {}
34    public method regoldenize {}
35
36    private variable _toolobj ""  ;# Rappture::Tool for tool being tested
37    private variable _testxml ""  ;# XML file for test case
38    private variable _testobj ""  ;# Rappture::Library object for _testxml
39
40    private variable _result "?"  ;# current status of this test
41    private variable _runobj ""   ;# results from last run
42    private variable _diffs ""    ;# diffs with respect to _runobj
43
44    private method _setWaiting {{newval ""}}
45    private method _setResult {name}
46    private method _computeDiffs {obj1 obj2 args}
47    private method _buildFailure {str}
48
49    # use this to add tests to the "run" queue
50    public proc queue {op args}
51
52    # useful helper function -- looks for val among choices
53    public proc oneof {choices val} {
54        return [expr {[lsearch -exact $choices $val] >= 0}]
55    }
56
57    private common _queue       ;# queue of objects waiting to run
58    set _queue(tests) ""        ;# list of tests in the queue
59    set _queue(pending) ""      ;# after event for "next" call
60    set _queue(running) ""      ;# test object currently running
61    set _queue(outputcmd) ""    ;# callback for reporting output
62}
63
64# ----------------------------------------------------------------------
65# CONSTRUCTOR
66# ----------------------------------------------------------------------
67itcl::body Rappture::Tester::Test::constructor {toolobj testxml args} {
68    set _toolobj $toolobj
69    set _testxml $testxml
70    set _testobj [Rappture::library $testxml]
71
72    eval configure $args
73}
74
75# ----------------------------------------------------------------------
76# DESTRUCTOR
77# ----------------------------------------------------------------------
78itcl::body Rappture::Tester::Test::destructor {} {
79    itcl::delete object $_testobj
80    # runobj can point to testobj if the test has just been
81    # regoldenized.  Don't try to delete twice.
82    if {$_runobj ne "" && $_runobj ne $_testobj} {
83        itcl::delete object $_runobj
84    }
85}
86
87# ----------------------------------------------------------------------
88# USAGE: getResult
89#
90# Returns the result of the test:
91#   ? ...... test hasn't been run yet
92#   Pass ... test ran recently and passed
93#   Fail ... test ran recently and failed
94#   Error ... test ran recently and run failed with an error
95# ----------------------------------------------------------------------
96itcl::body Rappture::Tester::Test::getResult {} {
97    return $_result
98}
99
100# ----------------------------------------------------------------------
101# USAGE: getTestInfo <path>
102# USAGE: getTestInfo children <path>
103# USAGE: getTestInfo element ?-as type? <path>
104#
105# Returns info about the Test case at the specified <path> in the XML.
106# If the <path> is missing or misspelled, this method returns ""
107# instead of an error.
108# ----------------------------------------------------------------------
109itcl::body Rappture::Tester::Test::getTestInfo {args} {
110    if {[llength $args] == 1} {
111        set path [lindex $args 0]
112        return [$_testobj get $path]
113    }
114    return [eval $_testobj $args]
115}
116
117# ----------------------------------------------------------------------
118# USAGE: getRunInfo <path>
119# USAGE: getRunInfo children <path>
120# USAGE: getRunInfo element ?-as type? <path>
121#
122# Returns info about the most recent run at the specified <path> in
123# the XML.  If the <path> is missing or misspelled, this method returns
124# "" instead of an error.
125# ----------------------------------------------------------------------
126itcl::body Rappture::Tester::Test::getRunInfo {args} {
127    if {[llength $args] == 1} {
128        set path [lindex $args 0]
129        return [$_runobj get $path]
130    }
131    return [eval $_runobj $args]
132}
133
134# ----------------------------------------------------------------------
135# USAGE: run ?-output callback path value path value ...?
136#
137# Kicks off a new simulation and checks the results against the golden
138# set of results.  Any arguments passed in are passed along to the
139# Tool object managing the run.  This may include parameter override
140# values and a callback for partial output.
141# ----------------------------------------------------------------------
142itcl::body Rappture::Tester::Test::run {args} {
143    # Delete existing library if rerun
144    if {$_runobj ne ""} {
145        itcl::delete object $_runobj
146        set _runobj ""
147        set _diffs ""
148    }
149
150    # copy inputs from the test into the run file
151    $_toolobj reset
152    foreach path [Rappture::entities -as path $_testobj input] {
153        if {[$_testobj element -as type $path.current] ne ""} {
154            lappend args $path [$_testobj get $path.current]
155        }
156    }
157
158    # run the test case...
159    _setResult "Running"
160    foreach {status result} [eval $_toolobj run $args] break
161
162    if {$status == 0} {
163        if {$result eq "ABORT"} {
164            _setResult "?"
165            return "aborted"
166        } elseif {[Rappture::library isvalid $result]} {
167            set _runobj $result
168
169            if {![oneof {0 ok} [$_testobj get output.status]]} {
170                # expected test to fail, but it didn't
171                set idiffs [_computeDiffs [$_toolobj xml object] $_runobj -in input]
172                set odiffs [_computeDiffs $_testobj $_runobj -what run]
173                set _diffs [concat $idiffs $odiffs]
174                _setResult "Fail"
175            } else {
176                set idiffs [_computeDiffs [$_toolobj xml object] $_testobj -in input]
177                set odiffs [_computeDiffs $_testobj $_runobj -in output]
178                set _diffs [concat $idiffs $odiffs]
179
180                # any differences from expected result mean test failed
181                if {[llength $_diffs] == 0} {
182                    _setResult "Pass"
183                } else {
184                    _setResult "Fail"
185                }
186            }
187            return "finished"
188        } else {
189            set _runobj [_buildFailure $result]
190            if {![oneof {0 ok} [$_testobj get output.status]]
191                  && [$_testobj get output.log] eq $result} {
192                _setResult "Pass"
193            } else {
194                set idiffs [_computeDiffs [$_toolobj xml object] $_runobj -in input]
195                set odiffs [_computeDiffs $_testobj $_runobj -what run]
196                set _diffs [concat $idiffs $odiffs]
197                _setResult "Fail"
198            }
199            return "finished"
200        }
201    } else {
202        set _runobj [_buildFailure $result]
203        if {![oneof {0 ok} [$_testobj get output.status]]
204              && [$_testobj get output.log] eq $result} {
205            _setResult "Pass"
206        } else {
207            set idiffs [_computeDiffs [$_toolobj xml object] $_runobj -in input]
208            set odiffs [_computeDiffs $_testobj $_runobj -what run]
209            set _diffs [concat $idiffs $odiffs]
210            _setResult "Fail"
211        }
212        return "finished"
213    }
214}
215
216# ----------------------------------------------------------------------
217# USAGE: abort
218#
219# Causes the current test kicked off by the "run" method to be aborted.
220# ----------------------------------------------------------------------
221itcl::body Rappture::Tester::Test::abort {} {
222    $_toolobj abort
223}
224
225# ----------------------------------------------------------------------
226# USAGE: regoldenize
227#
228# Regoldenize the test by overwriting the test xml containin the golden
229# results with the data in the runfile generated by the last run.  Copy
230# test label and description into the new file.  Update the test's
231# result attributes to reflect the changes. Throws an error if the test
232# has not been run.
233#
234# After regoldenizing, _testobj and _runobj will both refer to the same
235# library object, and the previous _runobj will be discarded.
236# ----------------------------------------------------------------------
237itcl::body Rappture::Tester::Test::regoldenize {} {
238    if {$_runobj eq ""} {
239        error "no test result to goldenize"
240    }
241    $_runobj put test.label [$_testobj get test.label]
242    $_runobj put test.description [$_testobj get test.description]
243
244    set fid [open $_testxml w]
245    puts $fid "<?xml version=\"1.0\"?>"
246    puts $fid [$_runobj xml]
247    close $fid
248
249    itcl::delete object $_testobj
250    set _testobj $_runobj
251    set _runobj ""
252
253    set _diffs ""
254    _setResult Pass
255}
256
257# ----------------------------------------------------------------------
258# USAGE: getDiffs ?<path>?
259#
260# With no extra args, this returns a list of paths that differ between
261# the golden and new results--either because the data values are
262# different, or because elements are missing or their attributes have
263# changed.
264#
265# If a particular <path> is specified, then detailed diffs are returned
266# for that path.  This is useful for "structure" diffs, where many
267# things may be different within a single object.
268# ----------------------------------------------------------------------
269itcl::body Rappture::Tester::Test::getDiffs {args} {
270    if {[llength $args] == 0} {
271        return $_diffs
272    } elseif {[llength $args] != 1} {
273        error "wrong # args: should be \"getDiffs ?path?\""
274    }
275
276    set path [lindex $args 0]
277    if {[string match input.* $path]} {
278        # if we're matching input, compare the original XML vs. the test
279        return [_computeDiffs [$_toolobj xml object] $_testobj -in $path -detail max]
280    }
281
282    # otherwise, compare the golden test vs. the test result
283    return [_computeDiffs $_testobj $_runobj -in $path -detail max]
284}
285
286# ----------------------------------------------------------------------
287# USAGE: getTestobj
288#
289# Returns a library object representing the test case.
290# ----------------------------------------------------------------------
291itcl::body Rappture::Tester::Test::getTestobj {} {
292    return $_testobj
293}
294
295# ----------------------------------------------------------------------
296# USAGE: getTestxml
297#
298# Returns the name of the xml file representing the test case.
299# ----------------------------------------------------------------------
300itcl::body Rappture::Tester::Test::getTestxml {} {
301    return $_testxml
302}
303
304# ----------------------------------------------------------------------
305# USAGE: _setResult ?|Pass|Fail|Waiting|Running
306#
307# Used internally to change the state of this test case.  If there
308# is a -notifycommand script for this object, it is invoked to notify
309# an interested client that the object has changed.
310# ----------------------------------------------------------------------
311itcl::body Rappture::Tester::Test::_setResult {name} {
312    set _result $name
313    if {[string length $notifycommand] > 0} {
314        uplevel #0 $notifycommand $this
315    }
316}
317
318# ----------------------------------------------------------------------
319# USAGE: _setWaiting ?boolean?
320#
321# Used to mark a Test as "waiting".  This usually happens when a test
322# is added to the queue, about to be run.
323# ----------------------------------------------------------------------
324itcl::body Rappture::Tester::Test::_setWaiting {{newval ""}} {
325    if {$newval ne "" && [string is boolean $newval]} {
326        if {$newval} {
327            _setResult "Waiting"
328        } else {
329            _setResult "?"
330        }
331    }
332    return $_result
333}
334
335# ----------------------------------------------------------------------
336# USAGE: _computeDiffs <xmlObj1> <xmlObj2> ?-in xxx? \
337#            ?-what value|attrs|run|all? ?-detail min|max?
338#
339# Used internally to compute differences between two different XML
340# objects.  This is normally used to look for differences between an
341# entire test case and a new run, but can also be used to look at
342# differences within a particular section or element via the -in flag.
343#
344# Returns a list of the following form:
345#     -what <diff> -path <path> -obj1 <xmlobj> -obj2 <xmlobj> \
346#           -v1 <value1> -v2 <value2>
347#
348#       where <diff> is one of:
349#         value - ....... element is missing from <xmlObj2>
350#         value c ....... element changed between <xmlObj1> and <xmlObj2>
351#         value + ....... element is missing from <xmlObj1>
352#         attrs c ....... attributes are different <xmlObj1> and <xmlObj2>
353#         type c ........ object types are different <xmlObj1> and <xmlObj2>
354#         attr - <path>.. attribute at <path> is missing from <xmlObj2>
355#         attr + <path>.. attribute at <path> is missing from <xmlObj1>
356#         attr c <path>.. attribute at <path> changed between objects
357# ----------------------------------------------------------------------
358itcl::body Rappture::Tester::Test::_computeDiffs {obj1 obj2 args} {
359    Rappture::getopts args params {
360        value -in output
361        value -what "attrs value"
362        value -detail min
363    }
364    if {$params(-what) == "all"} {
365        set params(-what) "attrs value run"
366    }
367
368    # handle any run output diffs first, so they appear at the top
369    # report this as one incident -- not separate reports for status/log
370    set rlist ""
371    if {[lsearch $params(-what) "run"] >= 0} {
372        set st1 [$obj1 get output.status]
373        set st2 [$obj2 get output.status]
374        if {$st1 ne $st2} {
375            # status changes are most serious
376            lappend rlist [list -what status -path output.status \
377                -obj1 $obj1 -obj2 $obj2]
378        } else {
379            set log1 [$obj1 get output.log]
380            set log2 [$obj2 get output.log]
381            if {$log1 ne $log2} {
382                # flag log changes instead if status agrees
383                lappend rlist [list -what status -path output.log \
384                    -obj1 $obj1 -obj2 $obj2]
385            }
386        }
387    }
388
389    # scan through the specified sections or paths
390    foreach elem $params(-in) {
391        if {[string first . $elem] >= 0} {
392            set v1paths $elem
393            set v2paths $elem
394        } else {
395            # query the values for all entities in both objects
396            set v1paths [Rappture::entities $obj1 $elem]
397            set v2paths [Rappture::entities $obj2 $elem]
398        }
399
400        # scan through values for obj1 and compare against obj2
401        foreach path $v1paths {
402            set details [list -path $path -obj1 $obj1 -obj2 $obj2]
403
404            set i [lsearch -exact $v2paths $path]
405            if {$i < 0} {
406                # missing from obj2
407                lappend rlist [linsert $details 0 -what "value -"]
408            } else {
409                foreach part $params(-what) {
410                    switch -- $part {
411                      value {
412                        set val1 [Rappture::objects::import $obj1 $path]
413                        set val2 [Rappture::objects::import $obj2 $path]
414                        lappend details -val1 $val1 -val2 $val2
415
416                        if {$val1 eq "" || $val2 eq ""} {
417                            lappend rlist [linsert $details 0 -what "value c"]
418                        } elseif {[$val1 info class] != [$val2 info class]} {
419                            lappend rlist [linsert $details 0 -what "value c"]
420                        } elseif {[$val1 compare $val2] != 0} {
421                            lappend rlist [linsert $details 0 -what "value c"]
422                        } else {
423                            itcl::delete object $val1 $val2
424                        }
425                        # handled this comparison
426                        set v2paths [lreplace $v2paths $i $i]
427                      }
428                      attrs {
429                        set what [list structure $path]
430                        set type1 [$obj1 element -as type $path]
431                        set type2 [$obj2 element -as type $path]
432                        if {$type1 eq $type2} {
433                          set same yes
434                          if {[catch {Rappture::objects::get $type1 -attributes} alist]} {
435                              # oops! unknown object type
436                              lappend rlist [linsert $details 0 -what unkobj]
437                              set alist ""
438                          }
439                          foreach rec $alist {
440                              array set attr [lrange $rec 1 end]
441                              set apath $path.$attr(-path)
442                              set v1 [$obj1 get $apath]
443                              set v2 [$obj2 get $apath]
444                              set dt [linsert $details end -v1 $v1 -v2 $v2]
445
446                              if {$v2 eq "" && $v1 ne ""} {
447                                  # missing from obj2
448                                  if {$params(-detail) == "max"} {
449                                      lappend rlist [linsert $dt 0 -what [list attr - $attr(-path)]]
450                                  } else {
451                                      set same no
452                                      break
453                                  }
454                              } elseif {$v1 eq "" && $v2 ne ""} {
455                                  # missing from obj1
456                                  if {$params(-detail) == "max"} {
457                                      lappend rlist [linsert $dt 0 -what [list attr + $attr(-path)]]
458                                  } else {
459                                      set same no
460                                      break
461                                  }
462                              } elseif {$v1 ne $v2} {
463                                  # different from obj2
464                                  if {$params(-detail) == "max"} {
465                                      lappend rlist [linsert $dt 0 -what [list attr c $attr(-path)]]
466                                  } else {
467                                      set same no
468                                      break
469                                  }
470                              }
471                          }
472                          if {$params(-detail) == "min" && !$same} {
473                              lappend details -what attrs
474                              lappend rlist [linsert $dt 0 -what "attrs c"]
475                          }
476                        } else {
477                          lappend details -val1 $type1 -val2 $type2
478                          lappend rlist [linsert $details 0 -what "type c"]
479                        }
480                      }
481                      run {
482                        # do nothing -- already handled above
483
484                        # handled this comparison
485                        set v2paths [lreplace $v2paths $i $i]
486                      }
487                      default {
488                        error "bad part \"$part\": should be attrs, value, run"
489                      }
490                    }
491                }
492            }
493        }
494
495        # add any values left over in the obj2
496        foreach path $v2paths {
497            set details [list -path $path -obj1 $obj1 -obj2 $obj2]
498            lappend rlist [linsert $details 0 -what "value +"]
499        }
500    }
501    return $rlist
502}
503
504# ----------------------------------------------------------------------
505# USAGE: _buildFailure <output>
506#
507# Returns a new Rappture::library object that contains a copy of the
508# original test with the given <output> and a failing status.  This
509# is used to represent the result of a test that aborts without
510# producing a valid run.xml file.
511# ----------------------------------------------------------------------
512itcl::body Rappture::Tester::Test::_buildFailure {output} {
513    set info "<?xml version=\"1.0\"?>\n[$_testobj xml]"
514    set obj [Rappture::LibraryObj ::#auto $info]
515    $obj remove test
516
517    $obj put output.time [clock format [clock seconds]]
518    $obj put output.status failed
519    $obj put output.user $::tcl_platform(user)
520    $obj put output.log $output
521
522    return $obj
523}
524
525# ======================================================================
526# RUN QUEUE
527# ======================================================================
528# USAGE: queue add <testObj> <testObj>...
529# USAGE: queue clear ?<testObj> <testObj>...?
530# USAGE: queue status <command>
531# USAGE: queue next
532# USAGE: queue output <string>
533#
534# Used to manipulate the run queue for the program as a whole.
535#
536# The "queue add" option adds the given <testObj> objects to the run
537# queue.  As soon as an object is added to the queue, it is marked
538# "waiting".  When it runs, it is marked "running", and it finally
539# goes to the "pass" or "fail" state.  If an object is already in
540# the queue, then this operation does nothing.
541#
542# The "queue clear" option clears specific objects from the queue.
543# If no objects are specified, then it clears all remaining objects.
544#
545# The "queue status" option is used to set the callback for handling
546# output from runs.  This command is called two ways:
547#    command start <testObj>
548#    command add <testObj> "string of output"
549#
550# The "queue next" option is used internally to run the next object
551# in the queue.  The "queue output" option is also used internally
552# to handle the output coming back from a run.  The output gets
553# shuttled along to the callback specified by "queue status".
554# ----------------------------------------------------------------------
555itcl::body Rappture::Tester::Test::queue {option args} {
556    switch -- $option {
557        add {
558            # add these tests to the run queue
559            foreach obj $args {
560                if {[catch {$obj isa Rappture::Tester::Test} valid] || !$valid} {
561                    error "bad value \"$obj\": should be Test object"
562                }
563                if {[lsearch $_queue(tests) $obj] < 0} {
564                    $obj _setWaiting 1
565                    lappend _queue(tests) $obj
566                }
567            }
568            if {$_queue(running) eq "" && $_queue(pending) eq ""} {
569                set _queue(pending) [after idle \
570                    Rappture::Tester::Test::queue next]
571            }
572        }
573        clear {
574            # remove these tests from the run queue
575            foreach obj $args {
576                if {[catch {$obj isa Rappture::Tester::Test} valid] || !$valid} {
577                    error "bad value \"$obj\": should be Test object"
578                }
579
580                # remove the test from the queue
581                set i [lsearch $_queue(tests) $obj]
582                if {$i >= 0} {
583                    set _queue(tests) [lreplace $_queue(tests) $i $i]
584                }
585
586                # mark object as no longer "waiting"
587                if {[$obj _setWaiting]} {
588                    $obj _setWaiting 0
589                }
590            }
591        }
592        status {
593            if {[llength $args] != 1} {
594                error "wrong # args: should be \"status command\""
595            }
596            set _queue(outputcmd) [lindex $args 0]
597        }
598        next {
599            set _queue(pending) ""
600
601            # get the next object from the queue
602            set obj [lindex $_queue(tests) 0]
603            set _queue(tests) [lrange $_queue(tests) 1 end]
604
605            if {$obj ne ""} {
606                set _queue(running) $obj
607                # invoke the callback to signal start of a run
608                if {[string length $_queue(outputcmd)] > 0} {
609                    uplevel #0 $_queue(outputcmd) start $obj
610                }
611
612                # run the test
613                set callback "Rappture::Tester::Test::queue output"
614                set status [$obj run -output $callback]
615                set _queue(running) ""
616
617                if {$status == "aborted"} {
618                    # if the test was aborted, clear any waiting tests
619                    Rappture::Tester::Test::queue clear
620                } elseif {[string match failed:* $status]} {
621                    bgerror $status
622                }
623
624                # set up to run the next test in the queue
625                set _queue(pending) [after idle \
626                    Rappture::Tester::Test::queue next]
627            }
628        }
629        output {
630            if {[llength $args] != 1} {
631                error "wrong # args: should be \"output string\""
632            }
633            if {[string length $_queue(outputcmd)] > 0} {
634                uplevel #0 $_queue(outputcmd) add $_queue(running) $args
635            }
636        }
637        default {
638            error "bad option \"$option\": should be add, clear, status, output, next"
639        }
640    }
641}
Note: See TracBrowser for help on using the repository browser.