0027691: Remove dchrono from all test cases and move its to perf group
[occt.git] / src / DrawResources / TestCommands.tcl
1 # Copyright (c) 2013-2014 OPEN CASCADE SAS
2 #
3 # This file is part of Open CASCADE Technology software library.
4 #
5 # This library is free software; you can redistribute it and/or modify it under
6 # the terms of the GNU Lesser General Public License version 2.1 as published
7 # by the Free Software Foundation, with special exception defined in the file
8 # OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
9 # distribution for complete text of the license and disclaimer of any warranty.
10 #
11 # Alternatively, this file may be used under the terms of Open CASCADE
12 # commercial license or contractual agreement.
13
14 ############################################################################
15 # This file defines scripts for execution of OCCT tests.
16 # It should be loaded automatically when DRAW is started, and provides
17 # top-level commands starting with 'test'. Type 'help test' to get their
18 # synopsys.
19 # See OCCT Tests User Guide for description of the test system.
20 #
21 # Note: procedures with names starting with underscore are for internal use 
22 # inside the test system.
23 ############################################################################
24
25 # Default verbose level for command _run_test
26 set _tests_verbose 0
27
28 # regexp for parsing test case results in summary log
29 set _test_case_regexp {^CASE\s+([\w.-]+)\s+([\w.-]+)\s+([\w.-]+)\s*:\s*([\w]+)(.*)}
30
31 # Basic command to run indicated test case in DRAW
32 help test {
33   Run specified test case
34   Use: test group grid casename [options...]
35   Allowed options are:
36   -echo: all commands and results are echoed immediately,
37          but log is not saved and summary is not produced
38          It is also possible to use "1" instead of "-echo"
39          If echo is OFF, log is stored in memory and only summary
40          is output (the log can be obtained with command "dlog get")
41   -outfile filename: set log file (should be non-existing),
42          it is possible to save log file in text file or
43          in html file(with snapshot), for that "filename"
44          should have ".html" extension
45   -overwrite: force writing log in existing file
46   -beep: play sound signal at the end of the test
47   -errors: show all lines from the log report that are recognized as errors
48          This key will be ignored if the "-echo" key is already set.
49 }
50 proc test {group grid casename {args {}}} {
51     # set default values of arguments
52     set echo 0
53     set errors 0
54     set logfile ""
55     set overwrite 0
56     set signal 0
57
58     # get test case paths (will raise error if input is invalid)
59     _get_test $group $grid $casename dir gridname casefile
60
61     # check arguments
62     for {set narg 0} {$narg < [llength $args]} {incr narg} {
63         set arg [lindex $args $narg]
64         # if echo specified as "-echo", convert it to bool
65         if { $arg == "-echo" || $arg == "1" } {
66             set echo t
67             continue
68         }
69
70         # output log file
71         if { $arg == "-outfile" } {
72             incr narg
73             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } {
74                 set logfile [lindex $args $narg]
75             } else {
76                 error "Option -outfile requires argument"
77             }
78             continue
79         }
80
81         # allow overwrite existing log
82         if { $arg == "-overwrite" } {
83             set overwrite 1
84             continue
85         }
86
87         # sound signal at the end of the test
88         if { $arg == "-beep" } {
89             set signal t
90             continue
91         }
92
93         # if errors specified as "-errors", convert it to bool
94         if { $arg == "-errors" } {
95             set errors t
96             continue
97         }
98
99         # unsupported option
100         error "Error: unsupported option \"$arg\""
101     }
102     # run test
103     uplevel _run_test $dir $group $gridname $casefile $echo 
104
105     # check log
106     if { !$echo } {
107         _check_log $dir $group $gridname $casename $errors [dlog get] summary html_log
108
109         # create log file
110         if { ! $overwrite && [file isfile $logfile] } {
111             error "Error: Specified log file \"$logfile\" exists; please remove it before running test or use -overwrite option"
112         }
113         if {$logfile != ""} {
114             if {[file extension $logfile] == ".html"} {
115                 if {[regexp {vdump ([^\s\n]+)} $html_log dump snapshot]} {
116                     catch {file copy -force $snapshot [file rootname $logfile][file extension $snapshot]}
117                 }
118                 _log_html $logfile $html_log "Test $group $grid $casename"
119             } else {
120                 _log_save $logfile "[dlog get]\n$summary" "Test $group $grid $casename"
121             }
122         }
123     }
124
125     # play sound signal at the end of test
126     if {$signal} {
127         puts "\7\7\7\7"
128     }
129     return
130 }
131
132 # Basic command to run indicated test case in DRAW
133 help testgrid {
134   Run all tests, or specified group, or one grid
135   Use: testgrid [groupmask [gridmask [casemask]]] [options...]
136   Allowed options are:
137   -parallel N: run N parallel processes (default is number of CPUs, 0 to disable)
138   -refresh N: save summary logs every N seconds (default 600, minimal 1, 0 to disable)
139   -outdir dirname: set log directory (should be empty or non-existing)
140   -overwrite: force writing logs in existing non-empty directory
141   -xml filename: write XML report for Jenkins (in JUnit-like format)
142   -beep: play sound signal at the end of the tests
143   -regress dirname: re-run only a set of tests that have been detected as regressions on some previous run.
144                     Here "dirname" is path to directory containing results of previous run.
145   Groups, grids, and test cases to be executed can be specified by list of file 
146   masks, separated by spaces or comma; default is all (*).
147 }
148 proc testgrid {args} {
149     global env tcl_platform _tests_verbose
150
151     ######################################################
152     # check arguments
153     ######################################################
154
155     # check that environment variable defining paths to test scripts is defined
156     if { ! [info exists env(CSF_TestScriptsPath)] || 
157         [llength $env(CSF_TestScriptsPath)] <= 0 } {
158         error "Error: Environment variable CSF_TestScriptsPath is not defined"
159     }
160
161     # treat options
162     set parallel [_get_nb_cpus]
163     set refresh 60
164     set logdir ""
165     set overwrite 0
166     set xmlfile ""
167     set signal 0
168     set regress 0
169     set prev_logdir ""
170     for {set narg 0} {$narg < [llength $args]} {incr narg} {
171         set arg [lindex $args $narg]
172
173         # parallel execution
174         if { $arg == "-parallel" } {
175             incr narg
176             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
177                 set parallel [expr [lindex $args $narg]]
178             } else {
179                 error "Option -parallel requires argument"
180             }
181             continue
182         }
183
184         # refresh logs time
185         if { $arg == "-refresh" } {
186             incr narg
187             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
188                 set refresh [expr [lindex $args $narg]]
189             } else {
190                 error "Option -refresh requires argument"
191             }
192             continue
193         }
194
195         # output directory
196         if { $arg == "-outdir" } {
197             incr narg
198             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
199                 set logdir [lindex $args $narg]
200             } else {
201                 error "Option -outdir requires argument"
202             }
203             continue
204         }
205
206         # allow overwrite logs 
207         if { $arg == "-overwrite" } {
208             set overwrite 1
209             continue
210         }
211
212         # refresh logs time
213         if { $arg == "-xml" } {
214             incr narg
215             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
216                 set xmlfile [lindex $args $narg]
217             }
218             if { $xmlfile == "" } {
219                 set xmlfile TESTS-summary.xml
220             }
221             continue
222         }
223
224         # sound signal at the end of the test
225         if { $arg == "-beep" } {
226             set signal t
227             continue
228         }
229
230         # re-run only a set of tests that have been detected as regressions on some previous run
231         if { $arg == "-regress" } {
232             incr narg
233             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } {
234                 set prev_logdir [lindex $args $narg]
235                 set regress 1
236             } else {
237                 error "Option -regress requires argument"
238             }
239             continue
240         }
241
242         # unsupported option
243         if { [regexp {^-} $arg] } {
244             error "Error: unsupported option \"$arg\""
245         }
246
247         # treat arguments not recognized as options as group and grid names
248         if { ! [info exists groupmask] } {
249             set groupmask [split $arg ,]
250         } elseif { ! [info exists gridmask] } {
251             set gridmask [split $arg ,]
252         } elseif { ! [info exists casemask] } {
253             set casemask [split $arg ,]
254         } else {
255             error "Error: cannot interpret argument $narg ($arg)"
256         }
257     }
258
259     # check that target log directory is empty or does not exist
260     set logdir [file normalize [string trim $logdir]]
261     set prev_logdir [file normalize [string trim $prev_logdir]]
262     if { $logdir == "" } {
263         # if specified logdir is empty string, generate unique name like 
264         # results/<branch>_<timestamp>
265         set prefix ""
266         if { ! [catch {exec git branch} gitout] &&
267              [regexp {[*] ([\w-]+)} $gitout res branch] } {
268             set prefix "${branch}_"
269         }
270         set logdir "results/${prefix}[clock format [clock seconds] -format {%Y-%m-%dT%H%M}]"
271
272         set logdir [file normalize $logdir]
273     }
274     if { [file isdirectory $logdir] && ! $overwrite && ! [catch {glob -directory $logdir *}] } {
275         error "Error: Specified log directory \"$logdir\" is not empty; please clean it before running tests"
276     } 
277     if { [catch {file mkdir $logdir}] || ! [file writable $logdir] } {
278         error "Error: Cannot create directory \"$logdir\", or it is not writable"
279     }
280
281     # masks for search of test groups, grids, and cases
282     if { ! [info exists groupmask] } { set groupmask * }
283     if { ! [info exists gridmask ] } { set gridmask  * }
284     if { ! [info exists casemask ] } { set casemask  * }
285
286     # Find test cases with FAILED and IMPROVEMENT statuses in previous run
287     # if option "regress" is given
288     set rerun_group_grid_case {}
289
290     if { ${regress} > 0 } {
291         if { "${groupmask}" != "*"} {
292             lappend rerun_group_grid_case [list $groupmask $gridmask $casemask]
293         }
294     } else {
295         lappend rerun_group_grid_case [list $groupmask $gridmask $casemask]
296     }
297
298     if { ${regress} > 0 } {
299         if { [file exists ${prev_logdir}/tests.log] } {
300             set fd [open ${prev_logdir}/tests.log]
301             while { [gets $fd line] >= 0 } {
302                 if {[regexp {CASE ([^\s]+) ([^\s]+) ([^\s]+): FAILED} $line dump group grid casename] ||
303                     [regexp {CASE ([^\s]+) ([^\s]+) ([^\s]+): IMPROVEMENT} $line dump group grid casename]} {
304                     lappend rerun_group_grid_case [list $group $grid $casename]
305                 }
306             }
307             close $fd
308         } else {
309             error "Error: file ${prev_logdir}/tests.log is not found, check your input arguments!"
310         }
311     }
312
313     ######################################################
314     # prepare list of tests to be performed
315     ######################################################
316
317     # list of tests, each defined by a list of:
318     # test scripts directory
319     # group (subfolder) name
320     # grid (subfolder) name
321     # test case name
322     # path to test case file
323     set tests_list {}
324
325     foreach group_grid_case ${rerun_group_grid_case} {
326         set groupmask [lindex $group_grid_case 0]
327         set gridmask  [lindex $group_grid_case 1]
328         set casemask  [lindex $group_grid_case 2]
329
330         # iterate by all script paths
331         foreach dir [lsort -unique [_split_path $env(CSF_TestScriptsPath)]] {
332             # protection against empty paths
333             set dir [string trim $dir]
334             if { $dir == "" } { continue }
335
336             if { $_tests_verbose > 0 } { _log_and_puts log "Examining tests directory $dir" }
337
338             # check that directory exists
339             if { ! [file isdirectory $dir] } {
340                 _log_and_puts log "Warning: directory $dir listed in CSF_TestScriptsPath does not exist, skipped"
341                 continue
342             }
343
344             # search all directories in the current dir with specified mask
345             if [catch {glob -directory $dir -tail -types d {*}$groupmask} groups] { continue }
346
347             # iterate by groups
348             if { $_tests_verbose > 0 } { _log_and_puts log "Groups to be executed: $groups" }
349             foreach group [lsort -dictionary $groups] {
350                 if { $_tests_verbose > 0 } { _log_and_puts log "Examining group directory $group" }
351
352                 # file grids.list must exist: it defines sequence of grids in the group
353                 if { ! [file exists $dir/$group/grids.list] } {
354                     _log_and_puts log "Warning: directory $dir/$group does not contain file grids.list, skipped"
355                     continue
356                 }
357
358                 # read grids.list file and make a list of grids to be executed
359                 set gridlist {}
360                 set fd [open $dir/$group/grids.list]
361                 set nline 0
362                 while { [gets $fd line] >= 0 } {
363                     incr nline
364
365                     # skip comments and empty lines
366                     if { [regexp "\[ \t\]*\#.*" $line] } { continue }
367                     if { [string trim $line] == "" } { continue }
368
369                     # get grid id and name
370                     if { ! [regexp "^\(\[0-9\]+\)\[ \t\]*\(\[A-Za-z0-9_.-\]+\)\$" $line res gridid grid] } {
371                         _log_and_puts log "Warning: cannot recognize line $nline in file $dir/$group/grids.list as \"gridid gridname\"; ignored"
372                         continue
373                     }
374
375                     # check that grid fits into the specified mask
376                     foreach mask $gridmask {
377                         if { $mask == $gridid || [string match $mask $grid] } {
378                             lappend gridlist $grid
379                         }
380                     }
381                 }
382                 close $fd
383
384                 # iterate by all grids
385                 foreach grid $gridlist {
386
387                     # check if this grid is aliased to another one
388                     set griddir $dir/$group/$grid
389                     if { [file exists $griddir/cases.list] } {
390                         set fd [open $griddir/cases.list]
391                         if { [gets $fd line] >= 0 } {
392                             set griddir [file normalize $dir/$group/$grid/[string trim $line]]
393                         }
394                         close $fd
395                     }
396
397                     # check if grid directory actually exists
398                     if { ! [file isdirectory $griddir] } {
399                         _log_and_puts log "Error: tests directory for grid $grid ($griddir) is missing; skipped"
400                         continue
401                     }
402
403                     # create directory for logging test results
404                     if { $logdir != "" } { file mkdir $logdir/$group/$grid }
405
406                     # iterate by all tests in the grid directory
407                     if { [catch {glob -directory $griddir -type f {*}$casemask} testfiles] } { continue }
408                     foreach casefile [lsort -dictionary $testfiles] {
409                         # filter out files with reserved names
410                         set casename [file tail $casefile]
411                         if { $casename == "begin" || $casename == "end" ||
412                              $casename == "parse.rules" } {
413                             continue
414                         }
415
416                         lappend tests_list [list $dir $group $grid $casename $casefile]
417                     }
418                 }
419             }
420         }
421     }
422     if { [llength $tests_list] < 1 } {
423         error "Error: no tests are found, check your input arguments and variable CSF_TestScriptsPath!"
424     } else {
425         puts "Running tests (total [llength $tests_list] test cases)..."
426     }
427
428     ######################################################
429     # run tests
430     ######################################################
431     
432     # log command arguments and environment
433     lappend log "Command: testgrid $args"
434     lappend log "Host: [info hostname]"
435     lappend log "Started on: [clock format [clock seconds] -format {%Y-%m-%d %H:%M:%S}]"
436     catch {lappend log "DRAW build:\n[dversion]" }
437     lappend log "Environment:"
438     foreach envar [lsort [array names env]] {
439         lappend log "$envar=\"$env($envar)\""
440     }
441     lappend log ""
442
443     set refresh_timer [clock seconds]
444     uplevel dchrono _timer reset
445     uplevel dchrono _timer start
446
447     # if parallel execution is requested, allocate thread pool
448     if { $parallel > 0 } {
449         if { ! [info exists tcl_platform(threaded)] || [catch {package require Thread}] } {
450             _log_and_puts log "Warning: Tcl package Thread is not available, running in sequential mode"
451             set parallel 0
452         } else {
453             set worker [tpool::create -minworkers $parallel -maxworkers $parallel]
454             # suspend the pool until all jobs are posted, to prevent blocking of the process
455             # of starting / processing jobs by running threads
456             catch {tpool::suspend $worker}
457             if { $_tests_verbose > 0 } { _log_and_puts log "Executing tests in (up to) $parallel threads" }
458             # limit number of jobs in the queue by reasonable value
459             # to prevent slowdown due to unnecessary queue processing
460             set nbpooled 0
461             set nbpooled_max [expr 10 * $parallel]
462             set nbpooled_ok  [expr  5 * $parallel]
463         }
464     }
465
466     # start test cases
467     set userbreak 0
468     foreach test_def $tests_list {
469         # check for user break
470         if { $userbreak || "[info commands dbreak]" == "dbreak" && [catch dbreak] } {
471             set userbreak 1
472             break
473         }
474
475         set dir       [lindex $test_def 0]
476         set group     [lindex $test_def 1]
477         set grid      [lindex $test_def 2]
478         set casename  [lindex $test_def 3]
479         set casefile  [lindex $test_def 4]
480
481         # command to set tests for generation of image in results directory
482         set imgdir_cmd ""
483         if { $logdir != "" } { set imgdir_cmd "set imagedir $logdir/$group/$grid" }
484
485         # prepare command file for running test case in separate instance of DRAW
486         set fd_cmd [open $logdir/$group/$grid/${casename}.tcl w]
487         puts $fd_cmd "$imgdir_cmd"
488         puts $fd_cmd "set test_image $casename"
489         puts $fd_cmd "_run_test $dir $group $grid $casefile t"
490
491         # use dlog command to obtain complete output of the test when it is absent (i.e. since OCCT 6.6.0)
492         # note: this is not needed if echo is set to 1 in call to _run_test above
493         if { ! [catch {dlog get}] } {
494             puts $fd_cmd "puts \[dlog get\]"
495         } else {
496             # else try to use old-style QA_ variables to get more output...
497             set env(QA_DUMP) 1
498             set env(QA_DUP) 1
499             set env(QA_print_command) 1
500         }
501
502         # final 'exit' is needed when running on Linux under VirtualGl
503         puts $fd_cmd "exit"
504         close $fd_cmd
505
506         # commant to run DRAW with a command file;
507         # note that empty string is passed as standard input to avoid possible 
508         # hang-ups due to waiting for stdin of the launching process
509         set command "exec <<{} DRAWEXE -f $logdir/$group/$grid/${casename}.tcl"
510
511         # alternative method to run without temporary file; disabled as it needs too many backslashes
512         # else {
513         # set command "exec <<\"\" DRAWEXE -c $imgdir_cmd\\\; set test_image $casename\\\; \
514         # _run_test $dir $group $grid $casefile\\\; \
515         # puts \\\[dlog get\\\]\\\; exit"
516         # }
517
518         # run test case, either in parallel or sequentially
519         if { $parallel > 0 } {
520             # parallel execution
521             set job [tpool::post -nowait $worker "catch \"$command\" output; return \$output"]
522             set job_def($job) [list $logdir $dir $group $grid $casename]
523             incr nbpooled
524             if { $nbpooled > $nbpooled_max } {
525                 _testgrid_process_jobs $worker $nbpooled_ok
526             }
527         } else {
528             # sequential execution
529             catch {eval $command} output
530             _log_test_case $output $logdir $dir $group $grid $casename log
531
532             # update summary log with requested period
533             if { $logdir != "" && $refresh > 0 && [expr [clock seconds] - $refresh_timer > $refresh] } {
534                 # update and dump summary
535                 _log_summarize $logdir $log
536                 set refresh_timer [clock seconds]
537             }
538         }
539     }
540
541     # get results of started threads
542     if { $parallel > 0 } {
543         _testgrid_process_jobs $worker
544         # release thread pool
545         if { $nbpooled > 0 } {
546             tpool::cancel $worker [array names job_def]
547         }
548         catch {tpool::resume $worker}
549         tpool::release $worker
550     }
551
552     uplevel dchrono _timer stop
553     set time [lindex [split [uplevel dchrono _timer show] "\n"] 0]
554
555     if { $userbreak } {
556         _log_and_puts log "*********** Stopped by user break ***********"
557         set time "${time} \nNote: the process is not finished, stopped by user break!"
558     }
559
560     ######################################################
561     # output summary logs and exit
562     ######################################################
563
564     _log_summarize $logdir $log $time
565     if { $logdir != "" } {
566         puts "Detailed logs are saved in $logdir"
567     }
568     if { $logdir != "" && $xmlfile != "" } {
569         # XML output file is assumed relative to log dir unless it is absolute
570         if { [ file pathtype $xmlfile] == "relative" } {
571             set xmlfile [file normalize $logdir/$xmlfile]
572         }
573         _log_xml_summary $logdir $xmlfile $log 0
574         puts "XML summary is saved to $xmlfile"
575     }
576     # play sound signal at the end of test
577     if {$signal} {
578         puts "\7\7\7\7"
579     }
580     return
581 }
582
583 # Procedure to regenerate summary log from logs of test cases
584 help testsummarize {
585   Regenerate summary log in the test directory from logs of test cases.
586   This can be necessary if test grids are executed separately (e.g. on
587   different stations) or some grids have been re-executed.
588   Use: testsummarize dir
589 }
590 proc testsummarize {dir} {
591     global _test_case_regexp
592
593     if { ! [file isdirectory $dir] } {
594         error "Error: \"$dir\" is not a directory"
595     }
596
597     # get summary statements from all test cases in one log
598     set log {}
599
600     # to avoid huge listing of logs, first find all subdirectories and iterate
601     # by them, parsing log files in each subdirectory independently 
602     foreach grid [glob -directory $dir -types d -tails */*] {
603         foreach caselog [glob -nocomplain -directory [file join $dir $grid] -types f -tails *.log] {
604             set file [file join $dir $grid $caselog]
605             set nbfound 0
606             set fd [open $file r]
607             while { [gets $fd line] >= 0 } {
608                 if { [regexp $_test_case_regexp $line res grp grd cas status message] } {
609                     if { "[file join $grid $caselog]" != "[file join $grp $grd ${cas}.log]" } { 
610                         puts "Error: $file contains status line for another test case ($line)"
611                     }
612                     lappend log $line
613                     incr nbfound
614                 }
615             }
616             close $fd
617
618             if { $nbfound != 1 } { 
619                 puts "Error: $file contains $nbfound status lines, expected 1"
620             }
621         }
622     }
623
624     _log_summarize $dir $log "Summary regenerated from logs at [clock format [clock seconds]]"
625     return
626 }
627
628 # Procedure to compare results of two runs of test cases
629 help testdiff {
630   Compare results of two executions of tests (CPU times, ...)
631   Use: testdiff dir1 dir2 [groupname [gridname]] [options...]
632   Where dir1 and dir2 are directories containing logs of two test runs.
633   Allowed options are:
634   -image [filename]: compare only images and save its in specified file (default 
635                    name is <dir1>/diffimage-<dir2>.log)
636   -cpu [filename]: compare only CPU and save it in specified file (default 
637                    name is <dir1>/diffcpu-<dir2>.log)
638   -memory [filename]: compare only memory and save it in specified file (default 
639                    name is <dir1>/diffmemory-<dir2>.log)
640   -save filename: save resulting log in specified file (default name is
641                   <dir1>/diff-<dir2>.log); HTML log is saved with same name
642                   and extension .html
643   -status {same|ok|all}: filter cases for comparing by their status:
644           same - only cases with same status are compared (default)
645           ok   - only cases with OK status in both logs are compared
646           all  - results are compared regardless of status
647   -verbose level: 
648           1 - output only differences 
649           2 - output also list of logs and directories present in one of dirs only
650           3 - (default) output also progress messages 
651   -highlight_percent value: highlight considerable (>value in %) deviations
652                             of CPU and memory (default value is 5%)
653 }
654 proc testdiff {dir1 dir2 args} {
655     if { "$dir1" == "$dir2" } {
656         error "Input directories are the same"
657     }
658
659     ######################################################
660     # check arguments
661     ######################################################
662
663     # treat options
664     set logfile [file join $dir1 "diff-[file tail $dir2].log"]
665     set logfile_image ""
666     set logfile_cpu ""
667     set logfile_memory ""
668     set image false
669     set cpu false
670     set memory false
671     set basename ""
672     set save false
673     set status "same"
674     set verbose 3
675     set highlight_percent 5
676     for {set narg 0} {$narg < [llength $args]} {incr narg} {
677         set arg [lindex $args $narg]
678         # log file name
679         if { $arg == "-save" } {
680             incr narg
681             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
682                 set logfile [lindex $args $narg]
683             } else {
684                 error "Error: Option -save must be followed by log file name"
685             } 
686             set save true
687             continue
688         }
689         
690         # image compared log
691         if { $arg == "-image" } {
692             incr narg
693             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
694                 set logfile_image [lindex $args $narg]
695             } else {
696                 set logfile_image [file join $dir1 "diffimage-[file tail $dir2].log"]
697                 incr narg -1
698             }
699             set image true
700             continue
701         }
702         
703         # CPU compared log
704         if { $arg == "-cpu" } {
705             incr narg
706             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
707                 set logfile_cpu [lindex $args $narg]
708             } else {
709                 set logfile_cpu [file join $dir1 "diffcpu-[file tail $dir2].log"]
710                 incr narg -1
711             }
712             set cpu true
713             continue
714         }
715         
716         # memory compared log
717         if { $arg == "-memory" } {
718             incr narg
719             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
720                 set logfile_memory [lindex $args $narg]
721             } else {
722                 set logfile_memory [file join $dir1 "diffmemory-[file tail $dir2].log"]
723                 incr narg -1
724             }
725             set memory true
726             continue
727         }
728         
729         # status filter
730         if { $arg == "-status" } {
731             incr narg
732             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
733                 set status [lindex $args $narg]
734             } else {
735                 set status ""
736             }
737             if { "$status" != "same" && "$status" != "all" && "$status" != "ok" } {
738                 error "Error: Option -status must be followed by one of \"same\", \"all\", or \"ok\""
739             }
740             continue
741         }
742
743         # verbose level
744         if { $arg == "-verbose" } {
745             incr narg
746             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
747                 set verbose [expr [lindex $args $narg]]
748             } else {
749                 error "Error: Option -verbose must be followed by integer verbose level"
750             }
751             continue
752         }
753
754         # highlight_percent
755         if { $arg == "-highlight_percent" } {
756             incr narg
757             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } { 
758                 set highlight_percent [expr [lindex $args $narg]]
759             } else {
760                 error "Error: Option -highlight_percent must be followed by integer value"
761             }
762             continue
763         }
764
765         if { [regexp {^-} $arg] } {
766             error "Error: unsupported option \"$arg\""
767         }
768
769         # non-option arguments form a subdirectory path
770         set basename [file join $basename $arg]
771     }
772     
773     if {$image != false || $cpu != false || $memory != false} {
774         if {$save != false} {
775             error "Error: Option -save can not be used with image/cpu/memory options"
776         }
777     }
778
779     # run diff procedure (recursive)
780     _test_diff $dir1 $dir2 $basename $image $cpu $memory $status $verbose log log_image log_cpu log_memory
781     
782     # save result to log file
783     if {$image == false && $cpu == false && $memory == false} {
784         if { "$logfile" != "" } {
785             _log_save $logfile [join $log "\n"]
786             _log_html_diff "[file rootname $logfile].html" $log $dir1 $dir2 ${highlight_percent}
787             puts "Log is saved to $logfile (and .html)"
788         }
789     } else {
790         foreach mode {image cpu memory} {
791             if {"[set logfile_${mode}]" != ""} {
792                 _log_save "[set logfile_${mode}]" [join "[set log_${mode}]" "\n"]
793                 _log_html_diff "[file rootname [set logfile_${mode}]].html" "[set log_${mode}]" $dir1 $dir2 ${highlight_percent}
794                 puts "Log (${mode}) is saved to [set logfile_${mode}] (and .html)"
795             }
796         }
797     }
798     return
799 }
800
801 # Procedure to check data file before adding it to repository
802 help testfile {
803   Check data file and prepare it for putting to test data files repository.
804   Use: testfile [filelist]
805
806   Will report if:
807   - data file (non-binary) is in DOS encoding (CR/LF)
808   - same data file (with same or another name) already exists in the repository
809   - another file with the same name already exists 
810   Note that names are assumed to be case-insensitive (for Windows).
811
812   Unless the file is already in the repository, tries to load it, reports
813   the recognized file format, file size, number of faces and edges in the 
814   loaded shape (if any), and makes snapshot (in the temporary directory).
815   Finally it advises whether the file should be put to public section of the 
816   repository.
817 }
818 proc testfile {filelist} {
819     global env
820
821     # check that CSF_TestDataPath is defined
822     if { ! [info exists env(CSF_TestDataPath)] } {
823         error "Environment variable CSF_TestDataPath must be defined!"
824     }
825
826     # build registry of existing data files (name -> path) and (size -> path)
827     puts "Checking available test data files..."
828     foreach dir [_split_path $env(CSF_TestDataPath)] {
829         while {[llength $dir] != 0} {
830             set curr [lindex $dir 0]
831             set dir [lrange $dir 1 end]
832             eval lappend dir [glob -nocomplain -directory $curr -type d *]
833             foreach file [glob -nocomplain -directory $curr -type f *] {
834                 set name [file tail $file]
835                 set name_lower [string tolower $name]
836
837                 # check that the file is not in DOS encoding
838                 if { [_check_dos_encoding $file] } {
839                     puts "Warning: file $file is in DOS encoding; was this intended?"
840                 }
841                 _check_file_format $file
842
843                 # check if file with the same name is present twice or more
844                 if { [info exists names($name_lower)] } {
845                     puts "Error: more than one file with name $name is present in the repository:"
846                     if { [_diff_files $file $names($name_lower)] } {
847                         puts "(files are different by content)"
848                     } else {
849                         puts "(files are same by content)"
850                     }
851                     puts "--> $file"
852                     puts "--> $names($name_lower)"
853                     continue
854                 } 
855                 
856                 # check if file with the same content exists
857                 set size [file size $file]
858                 if { [info exists sizes($size)] } {
859                     foreach other $sizes($size) {
860                         if { ! [_diff_files $file $other] } {
861                             puts "Warning: two files with the same content found:"
862                             puts "--> $file"
863                             puts "--> $other"
864                         }
865                     }
866                 }
867
868                 # add the file to the registry
869                 set names($name_lower) $file
870                 lappend sizes($size) $file
871             }
872         }
873     }
874     if { [llength $filelist] <= 0 } { return }
875
876     # check the new files
877     set has_images f
878     puts "Checking new file(s)..."
879     foreach file $filelist {
880         # check for DOS encoding
881         if { [_check_dos_encoding $file] } {
882             puts "$file: Warning: DOS encoding detected"
883         }
884
885         set name [file tail $file]
886         set name_lower [string tolower $name]
887
888         # check for presence of the file with same name
889         if { [info exists names($name_lower)] } {
890             if { [_diff_files $file $names($name_lower)] } {
891                 puts "$file: Error: name is already used by existing file\n--> $names($name_lower)"
892             } else {
893                 puts "$file: OK: already in the repository \n--> $names($name_lower)"
894                 continue
895             }
896         }
897                 
898         # check if file with the same content exists
899         set size [file size $file]
900         if { [info exists sizes($size)] } {
901             set found f
902             foreach other $sizes($size) {
903                 if { ! [_diff_files $file $other] } {
904                     puts "$file: OK: the same file is already present under name [file tail $other]\n--> $other"
905                     set found t
906                     break
907                 }
908             }
909             if { $found } { continue }
910         }
911
912         # try to read the file
913         set format [_check_file_format $file]
914         if { [catch {uplevel load_data_file $file $format a}] } {
915             puts "$file: Error: Cannot read as $format file"
916             continue
917         }
918
919         # get number of faces and edges
920         set edges 0
921         set faces 0
922         set nbs [uplevel nbshapes a]
923         regexp {EDGE[ \t:]*([0-9]+)} $nbs res edges
924         regexp {FACE[ \t:]*([0-9]+)} $nbs res faces
925
926         # classify; first check file size and number of faces and edges
927         if { $size < 95000 && $faces < 20 && $edges < 100 } {
928             set dir public
929         } else {
930             set dir private
931             # check if one of names of that file corresponds to typical name for 
932             # MDTV bugs or has extension .rle, this should be old model
933             if { [regexp -nocase {.*(cts|ats|pro|buc|ger|fra|usa|uki)[0-9]+.*} $name] ||
934                  [regexp -nocase {[.]rle\y} $name] } {
935                 set dir old
936             }
937         }
938
939         # add stats
940         puts "$file: $format size=[expr $size / 1024] KiB, nbfaces=$faces, nbedges=$edges -> $dir"
941
942         set tmpdir [_get_temp_dir]
943         file mkdir $tmpdir/$dir
944
945         # make snapshot
946         pload AISV
947         uplevel vdisplay a
948         uplevel vfit
949         uplevel vzfit
950         uplevel vdump $tmpdir/$dir/[file rootname [file tail $file]].png
951         set has_images t
952     }
953     if { $has_images } {
954         puts "Snapshots are saved in subdirectory [_get_temp_dir]"
955     }
956 }
957
958 # Procedure to locate data file for test given its name.
959 # The search is performed assuming that the function is called
960 # from the test case script; the search order is:
961 # - subdirectory "data" of the test script (grid) folder
962 # - subdirectories in environment variable CSF_TestDataPath
963 # - subdirectory set by datadir command
964 # If file is not found, raises Tcl error.
965 proc locate_data_file {filename} {
966     global env groupname gridname casename
967
968     # check if the file is located in the subdirectory data of the script dir
969     set scriptfile [info script]
970     if { $scriptfile != "" } {
971         set path [file join [file dirname $scriptfile] data $filename]
972         if { [file exists $path] } {
973             return [file normalize $path]
974         }
975     }
976
977     # check sub-directories in paths indicated by CSF_TestDataPath
978     if { [info exists env(CSF_TestDataPath)] } {
979         foreach dir [_split_path $env(CSF_TestDataPath)] {
980             while {[llength $dir] != 0} { 
981                 set name [lindex $dir 0]
982                 set dir [lrange $dir 1 end]
983                 # skip directories starting with dot
984                 if { [regexp {^[.]} $name] } { continue }
985                 if { [file exists $name/$filename] } {
986                     return [file normalize $name/$filename]
987                 }
988                 eval lappend dir [glob -nocomplain -directory $name -type d *]
989             }
990         }
991     }
992
993     # check current datadir
994     if { [file exists [uplevel datadir]/$filename] } {
995         return [file normalize [uplevel datadir]/$filename]
996     }
997
998     # raise error
999     error [join [list "File $filename could not be found" \
1000                       "(should be in paths indicated by CSF_TestDataPath environment variable, " \
1001                       "or in subfolder data in the script directory)"] "\n"]
1002 }
1003
1004 # Internal procedure to find test case indicated by group, grid, and test case names;
1005 # returns:
1006 # - dir: path to the base directory of the tests group
1007 # - gridname: actual name of the grid
1008 # - casefile: path to the test case script 
1009 # if no such test is found, raises error with appropriate message
1010 proc _get_test {group grid casename _dir _gridname _casefile} {
1011     upvar $_dir dir
1012     upvar $_gridname gridname
1013     upvar $_casefile casefile
1014
1015     global env
1016  
1017     # check that environment variable defining paths to test scripts is defined
1018     if { ! [info exists env(CSF_TestScriptsPath)] || 
1019          [llength $env(CSF_TestScriptsPath)] <= 0 } {
1020         error "Error: Environment variable CSF_TestScriptsPath is not defined"
1021     }
1022
1023     # iterate by all script paths
1024     foreach dir [_split_path $env(CSF_TestScriptsPath)] {
1025         # protection against empty paths
1026         set dir [string trim $dir]
1027         if { $dir == "" } { continue }
1028
1029         # check that directory exists
1030         if { ! [file isdirectory $dir] } {
1031             puts "Warning: directory $dir listed in CSF_TestScriptsPath does not exist, skipped"
1032             continue
1033         }
1034
1035         # check if test group with given name exists in this dir
1036         # if not, continue to the next test dir
1037         if { ! [file isdirectory $dir/$group] } { continue }
1038
1039         # check that grid with given name (possibly alias) exists; stop otherwise
1040         set gridname $grid
1041         if { ! [file isdirectory $dir/$group/$gridname] } {
1042             # check if grid is named by alias rather than by actual name
1043             if { [file exists $dir/$group/grids.list] } {
1044                 set fd [open $dir/$group/grids.list]
1045                 while { [gets $fd line] >= 0 } {
1046                     if { [regexp "\[ \t\]*\#.*" $line] } { continue }
1047                     if { [regexp "^$grid\[ \t\]*\(\[A-Za-z0-9_.-\]+\)\$" $line res gridname] } {
1048                         break
1049                     }
1050                 }
1051                 close $fd
1052             }
1053         }
1054         if { ! [file isdirectory $dir/$group/$gridname] } { continue }
1055
1056         # get actual file name of the script; stop if it cannot be found
1057         set casefile $dir/$group/$gridname/$casename
1058         if { ! [file exists $casefile] } {
1059             # check if this grid is aliased to another one
1060             if { [file exists $dir/$group/$gridname/cases.list] } {
1061                 set fd [open $dir/$group/$gridname/cases.list]
1062                 if { [gets $fd line] >= 0 } {
1063                     set casefile [file normalize $dir/$group/$gridname/[string trim $line]/$casename]
1064                 }
1065                 close $fd
1066             }
1067         }
1068         if { [file exists $casefile] } { 
1069             # normal return
1070             return 
1071         }
1072     }
1073
1074     # coming here means specified test is not found; report error
1075     error [join [list "Error: test case $group / $grid / $casename is not found in paths listed in variable" \
1076                       "CSF_TestScriptsPath (current value is \"$env(CSF_TestScriptsPath)\")"] "\n"]
1077 }
1078
1079 # Internal procedure to run test case indicated by base directory, 
1080 # grid and grid names, and test case file path.
1081 # The log can be obtained by command "dlog get".
1082 proc _run_test {scriptsdir group gridname casefile echo} {
1083     global env
1084
1085     # start timer
1086     uplevel dchrono _timer reset
1087     uplevel dchrono _timer start
1088     catch {uplevel meminfo h} membase
1089
1090     # enable commands logging; switch to old-style mode if dlog command is not present
1091     set dlog_exists 1
1092     if { [catch {dlog reset}] } {
1093         set dlog_exists 0
1094     } elseif { $echo } {
1095         decho on
1096     } else {
1097         dlog reset
1098         dlog on
1099         rename puts puts-saved
1100         proc puts args { 
1101             global _tests_verbose
1102
1103             # log only output to stdout and stderr, not to file!
1104             if {[llength $args] > 1} {
1105                 set optarg [lindex $args end-1]
1106                 if { $optarg == "stdout" || $optarg == "stderr" || $optarg == "-newline" } {
1107                     dlog add [lindex $args end]
1108                 } else {
1109                     eval puts-saved $args
1110                 }
1111             } else {
1112                 dlog add [lindex $args end]
1113             }
1114         }
1115     }
1116
1117     # evaluate test case 
1118     set tmp_imagedir 0
1119     if [catch {
1120         # set variables identifying test case
1121         uplevel set casename [file tail $casefile]
1122         uplevel set groupname $group
1123         uplevel set gridname $gridname
1124         uplevel set dirname  $scriptsdir
1125
1126         # set path for saving of log and images (if not yet set) to temp dir
1127         if { ! [uplevel info exists imagedir] } {
1128             uplevel set test_image \$casename
1129
1130             # create subdirectory in temp named after group and grid with timestamp
1131             set rootlogdir [_get_temp_dir]
1132         
1133             set imagedir "${group}-${gridname}-${::casename}-[clock format [clock seconds] -format {%Y-%m-%dT%Hh%Mm%Ss}]"
1134             set imagedir [file normalize ${rootlogdir}/$imagedir]
1135
1136             if { [catch {file mkdir $imagedir}] || ! [file writable $imagedir] ||
1137                  ! [catch {glob -directory $imagedir *}] } {
1138                  # puts "Warning: Cannot create directory \"$imagedir\", or it is not empty; \"${rootlogdir}\" is used"
1139                 set imagedir $rootlogdir
1140             }
1141
1142             uplevel set imagedir \"$imagedir\"
1143             set tmp_imagedir 1
1144         }
1145
1146         # execute test scripts 
1147         if { [file exists $scriptsdir/$group/begin] } {
1148             puts "Executing $scriptsdir/$group/begin..."; flush stdout
1149             uplevel source $scriptsdir/$group/begin
1150         }
1151         if { [file exists $scriptsdir/$group/$gridname/begin] } {
1152             puts "Executing $scriptsdir/$group/$gridname/begin..."; flush stdout
1153             uplevel source $scriptsdir/$group/$gridname/begin
1154         }
1155
1156         puts "Executing $casefile..."; flush stdout
1157         uplevel source $casefile
1158
1159         if { [file exists $scriptsdir/$group/$gridname/end] } {
1160             puts "Executing $scriptsdir/$group/$gridname/end..."; flush stdout
1161             uplevel source $scriptsdir/$group/$gridname/end
1162         }
1163         if { [file exists $scriptsdir/$group/end] } {
1164             puts "Executing $scriptsdir/$group/end..."; flush stdout
1165             uplevel source $scriptsdir/$group/end
1166         }
1167     } res] {
1168         puts "Tcl Exception: $res"
1169     }
1170
1171     # stop logging
1172     if { $dlog_exists } {
1173         if { $echo } {
1174             decho off
1175         } else {
1176             rename puts {}
1177             rename puts-saved puts
1178             dlog off
1179         }
1180     }
1181
1182     # stop cpulimit killer if armed by the test
1183     cpulimit
1184
1185     # add memory and timing info
1186     set stats ""
1187     if { ! [catch {uplevel meminfo h} memuse] } {
1188         append stats "MEMORY DELTA: [expr ($memuse - $membase) / 1024] KiB\n"
1189     }
1190     uplevel dchrono _timer stop
1191     set time [uplevel dchrono _timer show]
1192     if { [regexp -nocase {CPU user time:[ \t]*([0-9.e-]+)} $time res cpu_usr] } {
1193         append stats "TOTAL CPU TIME: $cpu_usr sec\n"
1194     }
1195     if { $dlog_exists && ! $echo } {
1196         dlog add $stats
1197     } else {
1198         puts $stats
1199     }
1200
1201     # unset global vars
1202     uplevel unset casename groupname gridname dirname
1203     if { $tmp_imagedir } { uplevel unset imagedir test_image }
1204 }
1205
1206 # Internal procedure to check log of test execution and decide if it passed or failed
1207 proc _check_log {dir group gridname casename errors log {_summary {}} {_html_log {}}} {
1208     global env
1209     if { $_summary != "" } { upvar $_summary summary }
1210     if { $_html_log != "" } { upvar $_html_log html_log }
1211     set summary {}
1212     set html_log {}
1213     set errors_log {}
1214
1215     if [catch {
1216
1217         # load definition of 'bad words' indicating test failure
1218         # note that rules are loaded in the order of decreasing priority (grid - group - common),
1219         # thus grid rules will override group ones
1220         set badwords {}
1221         foreach rulesfile [list $dir/$group/$gridname/parse.rules $dir/$group/parse.rules $dir/parse.rules] {
1222             if [catch {set fd [open $rulesfile r]}] { continue }
1223             while { [gets $fd line] >= 0 } {
1224                 # skip comments and empty lines
1225                 if { [regexp "\[ \t\]*\#.*" $line] } { continue }
1226                 if { [string trim $line] == "" } { continue }
1227                 # extract regexp
1228                 if { ! [regexp {^([^/]*)/([^/]*)/(.*)$} $line res status rexp comment] } { 
1229                     puts "Warning: cannot recognize parsing rule \"$line\" in file $rulesfile"
1230                     continue 
1231                 }
1232                 set status [string trim $status]
1233                 if { $comment != "" } { append status " ([string trim $comment])" }
1234                 set rexp [regsub -all {\\b} $rexp {\\y}] ;# convert regexp from Perl to Tcl style
1235                 lappend badwords [list $status $rexp]
1236             }
1237             close $fd
1238         }
1239         if { [llength $badwords] <= 0 } { 
1240             puts "Warning: no definition of error indicators found (check files parse.rules)" 
1241         }
1242
1243         # analyse log line-by-line
1244         set todos {} ;# TODO statements
1245         set requs {} ;# REQUIRED statements
1246         set todo_incomplete -1
1247         set status ""
1248         foreach line [split $log "\n"] {
1249             # check if line defines specific treatment of some messages
1250             if [regexp -nocase {^[ \s]*TODO ([^:]*):(.*)$} $line res platforms pattern] {
1251                 if { ! [regexp -nocase {\mAll\M} $platforms] && 
1252                      ! [regexp -nocase "\\m[checkplatform]\\M" $platforms] } {
1253                     lappend html_log [_html_highlight IGNORE $line]
1254                     continue ;# TODO statement is for another platform
1255                 }
1256
1257                 # record TODOs that mark unstable cases
1258                 if { [regexp {[\?]} $platforms] } {
1259                     set todos_unstable([llength $todos]) 1
1260                 }
1261
1262                 # convert legacy regexps from Perl to Tcl style
1263                 set pattern [regsub -all {\\b} [string trim $pattern] {\\y}]
1264
1265                 # special case: TODO TEST INCOMPLETE
1266                 if { [string trim $pattern] == "TEST INCOMPLETE" } {
1267                     set todo_incomplete [llength $todos]
1268                 }
1269
1270                 lappend todos [list $pattern [llength $html_log] $line]
1271                 lappend html_log [_html_highlight BAD $line]
1272                 continue
1273             }
1274             if [regexp -nocase {^[ \s]*REQUIRED ([^:]*):[ \s]*(.*)$} $line res platforms pattern] {
1275                 if { ! [regexp -nocase {\mAll\M} $platforms] && 
1276                      ! [regexp -nocase "\\m[checkplatform]\\M" $platforms] } {
1277                     lappend html_log [_html_highlight IGNORE $line]
1278                     continue ;# REQUIRED statement is for another platform
1279                 }
1280                 lappend requs [list $pattern [llength $html_log] $line]
1281                 lappend html_log [_html_highlight OK $line]
1282                 continue
1283             }
1284
1285             # check for presence of required messages 
1286             set ismarked 0
1287             for {set i 0} {$i < [llength $requs]} {incr i} {
1288                 set pattern [lindex $requs $i 0]
1289                 if { [regexp $pattern $line] } {
1290                     incr required_count($i)
1291                     lappend html_log [_html_highlight OK $line]
1292                     set ismarked 1
1293                     continue
1294                 }
1295             }
1296             if { $ismarked } {
1297                 continue
1298             }
1299
1300             # check for presence of messages indicating test result
1301             foreach bw $badwords {
1302                 if { [regexp [lindex $bw 1] $line] } { 
1303                     # check if this is known bad case
1304                     set is_known 0
1305                     for {set i 0} {$i < [llength $todos]} {incr i} {
1306                         set pattern [lindex $todos $i 0]
1307                         if { [regexp $pattern $line] } {
1308                             set is_known 1
1309                             incr todo_count($i)
1310                             lappend html_log [_html_highlight BAD $line]
1311                             break
1312                         }
1313                     }
1314
1315                     # if it is not in todo, define status
1316                     if { ! $is_known } {
1317                         set stat [lindex $bw 0 0]
1318                         if {$errors} {
1319                             lappend errors_log $line
1320                         }
1321                         lappend html_log [_html_highlight $stat $line]
1322                         if { $status == "" && $stat != "OK" && ! [regexp -nocase {^IGNOR} $stat] } {
1323                             set status [lindex $bw 0]
1324                         }
1325                     }
1326                     set ismarked 1
1327                     break
1328                 }
1329             }
1330             if { ! $ismarked } { 
1331                lappend html_log $line
1332             }
1333         }
1334
1335         # check for presence of TEST COMPLETED statement
1336         if { $status == "" && ! [regexp {TEST COMPLETED} $log] } {
1337             # check whether absence of TEST COMPLETED is known problem
1338             if { $todo_incomplete >= 0 } {
1339                 incr todo_count($todo_incomplete)
1340             } else {
1341                 set status "FAILED (no final message is found)"
1342             }
1343         }
1344
1345         # report test as failed if it doesn't contain required pattern
1346         if { $status == "" } {
1347             for {set i 0} {$i < [llength $requs]} {incr i} {
1348                 if { ! [info exists required_count($i)] } {
1349                     set linenum [lindex $requs $i 1]
1350                     set html_log [lreplace $html_log $linenum $linenum [_html_highlight FAILED [lindex $requs $i 2]]]
1351                     set status "FAILED (REQUIRED statement no. [expr $i + 1] is not found)"
1352                 }
1353             }
1354         }
1355
1356         # check declared bad cases and diagnose possible improvement 
1357         # (bad case declared but not detected).
1358         # Note that absence of the problem marked by TODO with question mark
1359         # (unstable) is not reported as improvement.
1360         if { $status == "" } {
1361             for {set i 0} {$i < [llength $todos]} {incr i} {
1362                 if { ! [info exists todos_unstable($i)] &&
1363                      (! [info exists todo_count($i)] || $todo_count($i) <= 0) } {
1364                     set linenum [lindex $todos $i 1]
1365                     set html_log [lreplace $html_log $linenum $linenum [_html_highlight IMPROVEMENT [lindex $todos $i 2]]]
1366                     set status "IMPROVEMENT (expected problem TODO no. [expr $i + 1] is not detected)"
1367                     break;
1368                 }
1369             }
1370         }
1371
1372         # report test as known bad if at least one of expected problems is found
1373         if { $status == "" && [llength [array names todo_count]] > 0 } {
1374             set status "BAD (known problem)"
1375         }
1376
1377         # report normal OK
1378         if { $status == "" } {set status "OK" }
1379
1380     } res] {
1381         set status "FAILED ($res)"
1382     }
1383
1384     # put final message
1385     _log_and_puts summary "CASE $group $gridname $casename: $status"
1386     set summary [join $summary "\n"]
1387     if {$errors} {
1388         foreach error $errors_log {
1389             _log_and_puts summary "  $error"
1390         }
1391     }
1392     set html_log "[_html_highlight [lindex $status 0] $summary]\n[join $html_log \n]"
1393 }
1394
1395 # Auxiliary procedure putting message to both cout and log variable (list)
1396 proc _log_and_puts {logvar message} {
1397     if { $logvar != "" } { 
1398         upvar $logvar log
1399         lappend log $message
1400     }
1401     puts $message
1402 }
1403
1404 # Auxiliary procedure to log result on single test case
1405 proc _log_test_case {output logdir dir group grid casename logvar} {
1406     upvar $logvar log
1407     set show_errors 0
1408     # check result and make HTML log
1409     _check_log $dir $group $grid $casename $show_errors $output summary html_log
1410     lappend log $summary
1411
1412     # save log to file
1413     if { $logdir != "" } {
1414         _log_html $logdir/$group/$grid/$casename.html $html_log "Test $group $grid $casename"
1415         _log_save $logdir/$group/$grid/$casename.log "$output\n$summary" "Test $group $grid $casename"
1416     }
1417 }
1418
1419 # Auxiliary procedure to save log to file
1420 proc _log_save {file log {title {}}} {
1421     # create missing directories as needed
1422     catch {file mkdir [file dirname $file]}
1423
1424     # try to open a file
1425     if [catch {set fd [open $file w]} res] {
1426         error "Error saving log file $file: $res"
1427     }
1428     
1429     # dump log and close
1430     puts $fd "$title\n"
1431     puts $fd $log
1432     close $fd
1433     return
1434 }
1435
1436 # Auxiliary procedure to make a (relative if possible) URL to a file for 
1437 # inclusion a reference in HTML log
1438 proc _make_url {htmldir file} {
1439     set htmlpath [file split [file normalize $htmldir]]
1440     set filepath [file split [file normalize $file]]
1441     for {set i 0} {$i < [llength $htmlpath]} {incr i} {
1442         if { "[lindex $htmlpath $i]" != "[lindex $filepath $i]" } {
1443             if { $i == 0 } { break }
1444             return "[string repeat "../" [expr [llength $htmlpath] - $i - 1]][eval file join [lrange $filepath $i end]]"
1445         }
1446     }
1447
1448     # if relative path could not be made, return full file URL
1449     return "file://[file normalize $file]"
1450 }
1451
1452 # Auxiliary procedure to save log to file
1453 proc _log_html {file log {title {}}} {
1454     # create missing directories as needed
1455     catch {file mkdir [file dirname $file]}
1456
1457     # try to open a file
1458     if [catch {set fd [open $file w]} res] {
1459         error "Error saving log file $file: $res"
1460     }
1461     
1462     # print header
1463     puts $fd "<html><head><title>$title</title></head><body><h1>$title</h1>"
1464
1465     # add images if present; these should have either PNG, GIF, or JPG extension,
1466     # and start with name of the test script, with optional suffix separated
1467     # by underscore or dash
1468     set imgbasename [file rootname [file tail $file]]
1469     foreach img [lsort [glob -nocomplain -directory [file dirname $file] -tails \
1470                              ${imgbasename}.gif   ${imgbasename}.png   ${imgbasename}.jpg \
1471                              ${imgbasename}_*.gif ${imgbasename}_*.png ${imgbasename}_*.jpg \
1472                              ${imgbasename}-*.gif ${imgbasename}-*.png ${imgbasename}-*.jpg]] {
1473         puts $fd "<p>[file tail $img]<br><img src=\"$img\"/><p>"
1474     }
1475
1476     # print log body, trying to add HTML links to script files on lines like
1477     # "Executing <filename>..."
1478     puts $fd "<pre>"
1479     foreach line [split $log "\n"] {
1480         if { [regexp {Executing[ \t]+([a-zA-Z0-9._/:-]+[^.])} $line res script] &&
1481              [file exists $script] } {
1482             set line [regsub $script $line "<a href=\"[_make_url $file $script]\">$script</a>"]
1483         }
1484         puts $fd $line
1485     }
1486     puts $fd "</pre></body></html>"
1487
1488     close $fd
1489     return
1490 }
1491
1492 # Auxiliary method to make text with HTML highlighting according to status
1493 proc _html_color {status} {
1494     # choose a color for the cell according to result
1495     if { $status == "OK" } { 
1496         return lightgreen
1497     } elseif { [regexp -nocase {^FAIL} $status] } { 
1498         return ff8080
1499     } elseif { [regexp -nocase {^BAD} $status] } { 
1500         return yellow
1501     } elseif { [regexp -nocase {^IMP} $status] } { 
1502         return orange
1503     } elseif { [regexp -nocase {^SKIP} $status] } { 
1504         return gray
1505     } elseif { [regexp -nocase {^IGNOR} $status] } { 
1506         return gray
1507     } else {
1508         puts "Warning: no color defined for status $status, using red as if FAILED"
1509         return red
1510     }
1511 }
1512
1513 # Format text line in HTML to be colored according to the status
1514 proc _html_highlight {status line} {
1515     return "<table><tr><td bgcolor=\"[_html_color $status]\">$line</td></tr></table>"
1516 }
1517
1518 # Internal procedure to generate HTML page presenting log of the tests
1519 # execution in tabular form, with links to reports on individual cases
1520 proc _log_html_summary {logdir log totals regressions improvements total_time} {
1521     global _test_case_regexp
1522
1523     # create missing directories as needed
1524     file mkdir $logdir
1525
1526     # try to open a file and start HTML
1527     if [catch {set fd [open $logdir/summary.html w]} res] {
1528         error "Error creating log file: $res"
1529     }
1530
1531     # write HRML header, including command to refresh log if still in progress
1532     puts $fd "<html><head>"
1533     puts $fd "<title>Tests summary</title>"
1534     if { $total_time == "" } {
1535         puts $fd "<meta http-equiv=\"refresh\" content=\"10\">"
1536     }
1537     puts $fd "<meta http-equiv=\"pragma\" content=\"NO-CACHE\">"
1538     puts $fd "</head><body>"
1539
1540     # put summary
1541     set legend(OK)          "Test passed OK"
1542     set legend(FAILED)      "Test failed (regression)"
1543     set legend(BAD)         "Known problem"
1544     set legend(IMPROVEMENT) "Possible improvement (expected problem not detected)"
1545     set legend(SKIPPED)     "Test skipped due to lack of data file"
1546     puts $fd "<h1>Summary</h1><table>"
1547     foreach nbstat $totals {
1548         set status [lindex $nbstat 1]
1549         if { [info exists legend($status)] } { 
1550             set comment $legend($status) 
1551         } else {
1552             set comment "User-defined status"
1553         }
1554         puts $fd "<tr><td align=\"right\">[lindex $nbstat 0]</td><td bgcolor=\"[_html_color $status]\">$status</td><td>$comment</td></tr>"
1555     }
1556     puts $fd "</table>"
1557
1558     # time stamp and elapsed time info
1559     if { $total_time != "" } { 
1560         puts $fd "<p>Generated on [clock format [clock seconds] -format {%Y-%m-%d %H:%M:%S}] on [info hostname]\n<p>"
1561         puts $fd [join [split $total_time "\n"] "<p>"]
1562     } else {
1563         puts $fd "<p>NOTE: This is intermediate summary; the tests are still running! This page will refresh automatically until tests are finished."
1564     }
1565    
1566     # print regressions and improvements
1567     foreach featured [list $regressions $improvements] {
1568         if { [llength $featured] <= 1 } { continue }
1569         set status [string trim [lindex $featured 0] { :}]
1570         puts $fd "<h2>$status</h2>"
1571         puts $fd "<table>"
1572         set groupgrid ""
1573         foreach test [lrange $featured 1 end] {
1574             if { ! [regexp {^(.*)\s+([\w.]+)$} $test res gg name] } {
1575                 set gg UNKNOWN
1576                 set name "Error building short list; check details"
1577             }
1578             if { $gg != $groupgrid } {
1579                 if { $groupgrid != "" } { puts $fd "</tr>" }
1580                 set groupgrid $gg
1581                 puts $fd "<tr><td>$gg</td>"
1582             }
1583             puts $fd "<td bgcolor=\"[_html_color $status]\"><a href=\"[regsub -all { } $gg /]/${name}.html\">$name</a></td>"
1584         }
1585         if { $groupgrid != "" } { puts $fd "</tr>" }
1586         puts $fd "</table>"
1587     }
1588
1589     # put detailed log with TOC
1590     puts $fd "<hr><h1>Details</h1>"
1591     puts $fd "<div style=\"float:right; padding: 10px; border-style: solid; border-color: blue; border-width: 2px;\">"
1592
1593     # process log line-by-line
1594     set group {}
1595     set letter {}
1596     set body {}
1597     foreach line [lsort -dictionary $log] {
1598         # check that the line is case report in the form "CASE group grid name: result (explanation)"
1599         if { ! [regexp $_test_case_regexp $line res grp grd casename result message] } {
1600             continue
1601         }
1602
1603         # start new group
1604         if { $grp != $group } {
1605             if { $letter != "" } { lappend body "</tr></table>" }
1606             set letter {}
1607             set group $grp
1608             set grid {}
1609             puts $fd "<a href=\"#$group\">$group</a><br>"
1610             lappend body "<h2><a name=\"$group\">Group $group</a></h2>"
1611         }
1612
1613         # start new grid
1614         if { $grd != $grid } {
1615             if { $letter != "" } { lappend body "</tr></table>" }
1616             set letter {}
1617             set grid $grd
1618             puts $fd "&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#$group-$grid\">$grid</a><br>"
1619             lappend body "<h2><a name=\"$group-$grid\">Grid $group $grid</a></h2>"
1620         }
1621
1622         # check if test case name is <letter><digit>; 
1623         # if not, set alnum to period "." to recognize non-standard test name
1624         if { ! [regexp {\A([A-Za-z]{1,2})([0-9]{1,2})\Z} $casename res alnum number] &&
1625              ! [regexp {\A([A-Za-z0-9]+)_([0-9]+)\Z} $casename res alnum number] } {
1626             set alnum $casename
1627         }
1628
1629         # start new row when letter changes or for non-standard names
1630         if { $alnum != $letter || $alnum == "." } {
1631             if { $letter != "" } { 
1632                 lappend body "</tr><tr>" 
1633             } else {
1634                 lappend body "<table><tr>"
1635             }
1636             set letter $alnum
1637         }    
1638
1639         lappend body "<td bgcolor=\"[_html_color $result]\"><a href=\"$group/$grid/${casename}.html\">$casename</a></td>"
1640     }
1641     puts $fd "</div>\n[join $body "\n"]</tr></table>"
1642
1643     # add remaining lines of log as plain text
1644     puts $fd "<h2>Plain text messages</h2>\n<pre>"
1645     foreach line $log {
1646         if { ! [regexp $_test_case_regexp $line] } {
1647             puts $fd "$line"
1648         }
1649     }
1650     puts $fd "</pre>"
1651
1652     # close file and exit
1653     puts $fd "</body>"
1654     close $fd
1655     return
1656 }
1657
1658 # Procedure to dump summary logs of tests
1659 proc _log_summarize {logdir log {total_time {}}} {
1660
1661     # sort log records alphabetically to have the same behavior on Linux and Windows 
1662     # (also needed if tests are run in parallel)
1663     set loglist [lsort -dictionary $log]
1664
1665     # classify test cases by status
1666     foreach line $loglist {
1667         if { [regexp {^CASE ([^:]*): ([[:alnum:]]+).*$} $line res caseid status] } {
1668             lappend stat($status) $caseid
1669         }
1670     }
1671     set totals {}
1672     set improvements {Improvements:}
1673     set regressions {Failed:}
1674     if { [info exists stat] } {
1675         foreach status [lsort [array names stat]] {
1676             lappend totals [list [llength $stat($status)] $status]
1677
1678             # separately count improvements (status starting with IMP) and regressions (all except IMP, OK, BAD, and SKIP)
1679             if { [regexp -nocase {^IMP} $status] } {
1680                 eval lappend improvements $stat($status)
1681             } elseif { $status != "OK" && ! [regexp -nocase {^BAD} $status] && ! [regexp -nocase {^SKIP} $status] } {
1682                 eval lappend regressions $stat($status)
1683             }
1684         }
1685     }
1686
1687     # if time is specified, add totals
1688     if { $total_time != "" } {
1689         if { [llength $improvements] > 1 } {
1690             _log_and_puts log [join $improvements "\n  "]
1691         }
1692         if { [llength $regressions] > 1 } {
1693             _log_and_puts log [join $regressions "\n  "]
1694         }
1695         if { [llength $improvements] == 1 && [llength $regressions] == 1 } {
1696             _log_and_puts log "No regressions"
1697         }
1698         _log_and_puts log "Total cases: [join $totals {, }]"
1699         _log_and_puts log $total_time
1700     }
1701
1702     # save log to files
1703     if { $logdir != "" } {
1704         _log_html_summary $logdir $log $totals $regressions $improvements $total_time
1705         _log_save $logdir/tests.log [join $log "\n"] "Tests summary"
1706     }
1707
1708     return
1709 }
1710
1711 # Internal procedure to generate XML log in JUnit style, for further
1712 # consumption by Jenkins or similar systems.
1713 #
1714 # The output is intended to conform to XML schema supported by Jenkins found at
1715 # https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd
1716 #
1717 # The mapping of the fields is inspired by annotated schema of Apache Ant JUnit XML format found at
1718 # http://windyroad.org/dl/Open%20Source/JUnit.xsd
1719 proc _log_xml_summary {logdir filename log include_cout} {
1720     global _test_case_regexp
1721
1722     catch {file mkdir [file dirname $filename]}
1723
1724     # try to open a file and start XML
1725     if [catch {set fd [open $filename w]} res] {
1726         error "Error creating XML summary file $filename: $res"
1727     }
1728     puts $fd "<?xml version='1.0' encoding='utf-8'?>"
1729     puts $fd "<testsuites>"
1730
1731     # prototype for command to generate test suite tag
1732     set time_and_host "timestamp=\"[clock format [clock seconds] -format {%Y-%m-%dT%H:%M:%S}]\" hostname=\"[info hostname]\""
1733     set cmd_testsuite {puts $fd "<testsuite name=\"$group $grid\" tests=\"$nbtests\" failures=\"$nbfail\" errors=\"$nberr\" time=\"$time\" skipped=\"$nbskip\" $time_and_host>\n$testcases\n</testsuite>\n"}
1734
1735     # sort log and process it line-by-line
1736     set group {}
1737     foreach line [lsort -dictionary $log] {
1738         # check that the line is case report in the form "CASE group grid name: result (explanation)"
1739         if { ! [regexp $_test_case_regexp $line res grp grd casename result message] } {
1740             continue
1741         }
1742         set message [string trim $message " \t\r\n()"]
1743
1744         # start new testsuite for each grid
1745         if { $grp != $group || $grd != $grid } {
1746
1747             # write previous test suite
1748             if [info exists testcases] { eval $cmd_testsuite }
1749
1750             set testcases {}
1751             set nbtests 0
1752             set nberr 0
1753             set nbfail 0
1754             set nbskip 0
1755             set time 0.
1756
1757             set group $grp
1758             set grid $grd
1759         }
1760
1761         incr nbtests
1762  
1763         # parse test log and get its CPU time
1764         set testout {}
1765         set add_cpu {}
1766         if { [catch {set fdlog [open $logdir/$group/$grid/${casename}.log r]} ret] } { 
1767             puts "Error: cannot open $logdir/$group/$grid/${casename}.log: $ret"
1768         } else {
1769             while { [gets $fdlog logline] >= 0 } {
1770                 if { $include_cout } {
1771                     append testout "$logline\n"
1772                 }
1773                 if [regexp -nocase {TOTAL CPU TIME:\s*([\d.]+)\s*sec} $logline res cpu] {
1774                     set add_cpu " time=\"$cpu\""
1775                     set time [expr $time + $cpu]
1776                 }
1777             }
1778             close $fdlog
1779         }
1780         if { ! $include_cout } {
1781             set testout "$line\n"
1782         }
1783
1784         # record test case with its output and status
1785         # Mapping is: SKIPPED, BAD, and OK to OK, all other to failure
1786         append testcases "\n  <testcase name=\"$casename\"$add_cpu status=\"$result\">\n"
1787         append testcases "\n    <system-out>\n$testout    </system-out>"
1788         if { $result != "OK" } {
1789             if { [regexp -nocase {^SKIP} $result] } {
1790                 incr nberr
1791                 append testcases "\n    <error name=\"$result\" message=\"$message\"/>"
1792             } elseif { [regexp -nocase {^BAD} $result] } {
1793                 incr nbskip
1794                 append testcases "\n    <skipped>$message</skipped>"
1795             } else {
1796                 incr nbfail
1797                 append testcases "\n    <failure name=\"$result\" message=\"$message\"/>"
1798             }
1799         }
1800         append testcases "\n  </testcase>"
1801     }
1802
1803     # write last test suite
1804     if [info exists testcases] { eval $cmd_testsuite }
1805
1806     # the end
1807     puts $fd "</testsuites>"
1808     close $fd
1809     return
1810 }
1811
1812 # Auxiliary procedure to split path specification (usually defined by
1813 # environment variable) into list of directories or files
1814 proc _split_path {pathspec} {
1815     global tcl_platform
1816
1817     # first replace all \ (which might occur on Windows) by /  
1818     regsub -all "\\\\" $pathspec "/" pathspec
1819
1820     # split path by platform-specific separator
1821     return [split $pathspec [_path_separator]]
1822 }
1823
1824 # Auxiliary procedure to define platform-specific separator for directories in
1825 # path specification
1826 proc _path_separator {} {
1827     global tcl_platform
1828
1829     # split path by platform-specific separator
1830     if { $tcl_platform(platform) == "windows" } {
1831         return ";"
1832     } else {
1833         return ":"
1834     }
1835 }
1836
1837 # Procedure to make a diff and common of two lists
1838 proc _list_diff {list1 list2 _in1 _in2 _common} {
1839     upvar $_in1 in1
1840     upvar $_in2 in2
1841     upvar $_common common
1842
1843     set in1 {}
1844     set in2 {}
1845     set common {}
1846     foreach item $list1 {
1847         if { [lsearch -exact $list2 $item] >= 0 } {
1848             lappend common $item
1849         } else {
1850             lappend in1 $item
1851         }
1852     }
1853     foreach item $list2 {
1854         if { [lsearch -exact $common $item] < 0 } {
1855             lappend in2 $item
1856         }
1857     }
1858     return
1859 }
1860
1861 # procedure to load a file to Tcl string
1862 proc _read_file {filename} {
1863     set fd [open $filename r]
1864     set result [read -nonewline $fd]
1865     close $fd
1866     return $result
1867 }
1868
1869 # procedure to construct name for the mage diff file
1870 proc _diff_img_name {dir1 dir2 casepath imgfile} {
1871     return [file join $dir1 $casepath "diff-[file tail $dir2]-$imgfile"]
1872 }
1873
1874 # auxiliary procedure to produce string comparing two values
1875 proc _diff_show_ratio {value1 value2} {
1876     return "$value1 / $value2 \[[format "%+5.2f%%" [expr 100 * ($value1 - $value2) / double($value2)]]\]"
1877 }
1878
1879 # procedure to check cpu user time
1880 proc _check_time {regexp_msg} {
1881     upvar log log
1882     upvar log1 log1
1883     upvar log2 log2
1884     upvar log_cpu log_cpu
1885     upvar cpu cpu
1886     upvar basename basename
1887     upvar casename casename
1888     set time1_list [dict create]
1889     set time2_list [dict create]
1890     set cpu_find UNDEFINED
1891
1892     foreach line1 [split $log1 "\n"] {
1893         if { [regexp "${regexp_msg}" $line1 dump chronometer_name cpu_find] } {
1894             dict set time1_list "${chronometer_name}" "${cpu_find}"
1895         }
1896     }
1897
1898     foreach line2 [split $log2 "\n"] {
1899         if { [regexp "${regexp_msg}" $line2 dump chronometer_name cpu_find] } {
1900             dict set time2_list "${chronometer_name}" "${cpu_find}"
1901         }
1902     }
1903
1904     if { [llength [dict keys $time1_list]] != [llength [dict keys $time2_list]] } {
1905         puts "Error: number of dchrono/chrono COUNTER are different in the same test cases"
1906     } else {
1907         foreach key [dict keys $time1_list] {
1908             set time1 [dict get $time1_list $key]
1909             set time2 [dict get $time2_list $key]
1910
1911             # compare CPU user time with 10% precision (but not less 0.5 sec)
1912             if { [expr abs ($time1 - $time2) > 0.5 + 0.05 * abs ($time1 + $time2)] } {
1913                 if {$cpu != false} {
1914                     _log_and_puts log_cpu "COUNTER $key: [split $basename /] $casename: [_diff_show_ratio $time1 $time2]"
1915                 } else {
1916                     _log_and_puts log "COUNTER $key: [split $basename /] $casename: [_diff_show_ratio $time1 $time2]"
1917                 }
1918             }
1919         }
1920     }
1921 }
1922
1923 # Procedure to compare results of two runs of test cases
1924 proc _test_diff {dir1 dir2 basename image cpu memory status verbose _logvar _logimage _logcpu _logmemory {_statvar ""}} {
1925     upvar $_logvar log
1926     upvar $_logimage log_image
1927     upvar $_logcpu log_cpu
1928     upvar $_logmemory log_memory
1929
1930     # make sure to load diffimage command
1931     uplevel pload VISUALIZATION
1932
1933     # prepare variable (array) for collecting statistics
1934     if { "$_statvar" != "" } {
1935         upvar $_statvar stat
1936     } else {
1937         set stat(cpu1) 0
1938         set stat(cpu2) 0
1939         set stat(mem1) 0
1940         set stat(mem2) 0
1941         set log {}
1942         set log_image {}
1943         set log_cpu {}
1944         set log_memory {}
1945     }
1946
1947     # first check subdirectories
1948     set path1 [file join $dir1 $basename]
1949     set path2 [file join $dir2 $basename]
1950     set list1 [glob -directory $path1 -types d -tails -nocomplain *]
1951     set list2 [glob -directory $path2 -types d -tails -nocomplain *]
1952     if { [llength $list1] >0 || [llength $list2] > 0 } {
1953         _list_diff $list1 $list2 in1 in2 common
1954         if { "$verbose" > 1 } {
1955             if { [llength $in1] > 0 } { _log_and_puts log "Only in $path1: $in1" }
1956             if { [llength $in2] > 0 } { _log_and_puts log "Only in $path2: $in2" }
1957         }
1958         foreach subdir $common {
1959             if { "$verbose" > 2 } {
1960                 _log_and_puts log "Checking [file join $basename $subdir]"
1961             }
1962             _test_diff $dir1 $dir2 [file join $basename $subdir] $image $cpu $memory $status $verbose log log_image log_cpu log_memory stat
1963         }
1964     } else {
1965         # check log files (only if directory has no subdirs)
1966         set list1 [glob -directory $path1 -types f -tails -nocomplain *.log]
1967         set list2 [glob -directory $path2 -types f -tails -nocomplain *.log]
1968         _list_diff $list1 $list2 in1 in2 common
1969         if { "$verbose" > 1 } {
1970             if { [llength $in1] > 0 } { _log_and_puts log "Only in $path1: $in1" }
1971             if { [llength $in2] > 0 } { _log_and_puts log "Only in $path2: $in2" }
1972         }
1973         set gcpu1 0
1974         set gcpu2 0
1975         set gmem1 0
1976         set gmem2 0
1977         foreach logfile $common {
1978             # load two logs
1979             set log1 [_read_file [file join $dir1 $basename $logfile]]
1980             set log2 [_read_file [file join $dir2 $basename $logfile]]
1981             set casename [file rootname $logfile]
1982             
1983             # check execution statuses
1984             if {$image == false && $cpu == false && $memory == false} {
1985                 set status1 UNDEFINED
1986                 set status2 UNDEFINED
1987                 if { ! [regexp {CASE [^:]*:\s*([\w]+)} $log1 res1 status1] ||
1988                     ! [regexp {CASE [^:]*:\s*([\w]+)} $log2 res2 status2] ||
1989                     "$status1" != "$status2" } {
1990                     _log_and_puts log "STATUS [split $basename /] $casename: $status1 / $status2"
1991                     # if test statuses are different, further comparison makes 
1992                     # no sense unless explicitly requested
1993                     if { "$status" != "all" } {
1994                         continue
1995                     }
1996                 }
1997                 if { "$status" == "ok" && "$status1" != "OK" } { 
1998                     continue
1999                 }
2000             }
2001
2002             if { ! $image } {
2003                 # check CPU user time in test cases
2004                 set checkCPURegexp "COUNTER (.+): (\[-0-9.+eE\]+)"
2005                 if { [regexp "${checkCPURegexp}" $log1] &&
2006                      [regexp "${checkCPURegexp}" $log2] } {
2007                   _check_time "${checkCPURegexp}"
2008                 }
2009             }
2010             
2011             # check CPU times
2012             if {$cpu != false || ($image == false && $cpu == false && $memory == false)} {
2013                 set cpu1 UNDEFINED
2014                 set cpu2 UNDEFINED
2015                 if { [regexp {TOTAL CPU TIME:\s*([\d.]+)} $log1 res1 cpu1] &&
2016                      [regexp {TOTAL CPU TIME:\s*([\d.]+)} $log2 res1 cpu2] } {
2017                     set stat(cpu1) [expr $stat(cpu1) + $cpu1]
2018                     set stat(cpu2) [expr $stat(cpu2) + $cpu2]
2019                     set gcpu1 [expr $gcpu1 + $cpu1]
2020                     set gcpu2 [expr $gcpu2 + $cpu2]
2021
2022                     # compare CPU times with 10% precision (but not less 0.5 sec)
2023                     if { [expr abs ($cpu1 - $cpu2) > 0.5 + 0.05 * abs ($cpu1 + $cpu2)] } {
2024                         if {$cpu != false} {
2025                             _log_and_puts log_cpu "CPU [split $basename /] $casename: [_diff_show_ratio $cpu1 $cpu2]"
2026                         } else {
2027                             _log_and_puts log "CPU [split $basename /] $casename: [_diff_show_ratio $cpu1 $cpu2]"
2028                         }
2029                     }
2030                 }
2031             }
2032
2033             # check memory delta
2034             if {$memory != false || ($image == false && $cpu == false && $memory == false)} {
2035                 set mem1 UNDEFINED
2036                 set mem2 UNDEFINED
2037                 if { [regexp {MEMORY DELTA:\s*([\d.]+)} $log1 res1 mem1] &&
2038                      [regexp {MEMORY DELTA:\s*([\d.]+)} $log2 res1 mem2] } {
2039                     set stat(mem1) [expr $stat(mem1) + $mem1]
2040                     set stat(mem2) [expr $stat(mem2) + $mem2]
2041                     set gmem1 [expr $gmem1 + $mem1]
2042                     set gmem2 [expr $gmem2 + $mem2]
2043
2044                     # compare memory usage with 10% precision (but not less 16 KiB)
2045                     if { [expr abs ($mem1 - $mem2) > 16 + 0.05 * abs ($mem1 + $mem2)] } {
2046                         if {$memory != false} {
2047                             _log_and_puts log_memory "MEMORY [split $basename /] $casename: [_diff_show_ratio $mem1 $mem2]"
2048                         } else {
2049                             _log_and_puts log "MEMORY [split $basename /] $casename: [_diff_show_ratio $mem1 $mem2]"
2050                         }
2051                     }
2052                 }
2053             }
2054
2055             # check images
2056             if {$image != false || ($image == false && $cpu == false && $memory == false)} {
2057                 set imglist1 [glob -directory $path1 -types f -tails -nocomplain ${casename}.{png,gif} ${casename}-*.{png,gif} ${casename}_*.{png,gif}]
2058                 set imglist2 [glob -directory $path2 -types f -tails -nocomplain ${casename}.{png,gif} ${casename}-*.{png,gif} ${casename}_*.{png,gif}]
2059                 _list_diff $imglist1 $imglist2 imgin1 imgin2 imgcommon
2060                 if { "$verbose" > 1 } {
2061                     if { [llength $imgin1] > 0 } {
2062                         if {$image != false} {
2063                             _log_and_puts log_image "Only in $path1: $imgin1"
2064                         } else {
2065                             _log_and_puts log "Only in $path1: $imgin1"
2066                         }
2067                     }
2068                     if { [llength $imgin2] > 0 } {
2069                         if {$image != false} {
2070                             _log_and_puts log_image "Only in $path2: $imgin2"
2071                         } else {
2072                             _log_and_puts log "Only in $path2: $imgin2"
2073                         }
2074                     }
2075                 }
2076                 foreach imgfile $imgcommon {
2077                     # if { $verbose > 1 } { _log_and_puts log "Checking [split basename /] $casename: $imgfile" }
2078                     set diffile [_diff_img_name $dir1 $dir2 $basename $imgfile]
2079                     if { [catch {diffimage [file join $dir1 $basename $imgfile] \
2080                                            [file join $dir2 $basename $imgfile] \
2081                                            0 0 0 $diffile} diff] } {
2082                         if {$image != false} {
2083                             _log_and_puts log_image "IMAGE [split $basename /] $casename: $imgfile cannot be compared"
2084                         } else {
2085                             _log_and_puts log "IMAGE [split $basename /] $casename: $imgfile cannot be compared"
2086                         }
2087                         file delete -force $diffile ;# clean possible previous result of diffimage
2088                     } elseif { $diff != 0 } {
2089                         if {$image != false} {
2090                             _log_and_puts log_image "IMAGE [split $basename /] $casename: $imgfile differs"
2091                         } else {
2092                             _log_and_puts log "IMAGE [split $basename /] $casename: $imgfile differs"
2093                         }
2094                     } else {
2095                         file delete -force $diffile ;# clean useless artifact of diffimage
2096                     }
2097                 }
2098             }
2099         }
2100         
2101         # report CPU and memory difference in group if it is greater than 10%
2102         if {$cpu != false || ($image == false && $cpu == false && $memory == false)} {
2103             if { [expr abs ($gcpu1 - $gcpu2) > 0.5 + 0.005 * abs ($gcpu1 + $gcpu2)] } {
2104                 if {$cpu != false} {
2105                     _log_and_puts log_cpu "CPU [split $basename /]: [_diff_show_ratio $gcpu1 $gcpu2]"
2106                 } else {
2107                     _log_and_puts log "CPU [split $basename /]: [_diff_show_ratio $gcpu1 $gcpu2]"
2108                 }
2109             }
2110         }
2111         if {$memory != false || ($image == false && $cpu == false && $memory == false)} {
2112             if { [expr abs ($gmem1 - $gmem2) > 16 + 0.005 * abs ($gmem1 + $gmem2)] } {
2113                 if {$memory != false} {
2114                     _log_and_puts log_memory "MEMORY [split $basename /]: [_diff_show_ratio $gmem1 $gmem2]"
2115                 } else {
2116                     _log_and_puts log "MEMORY [split $basename /]: [_diff_show_ratio $gmem1 $gmem2]"
2117                 }
2118             }
2119         }
2120     }
2121
2122     if { "$_statvar" == "" } {
2123         if {$memory != false || ($image == false && $cpu == false && $memory == false)} {
2124             if {$memory != false} {
2125                 _log_and_puts log_memory "Total MEMORY difference: [_diff_show_ratio $stat(mem1) $stat(mem2)]"
2126             } else {
2127                 _log_and_puts log "Total MEMORY difference: [_diff_show_ratio $stat(mem1) $stat(mem2)]"
2128             }
2129         }
2130         if {$cpu != false || ($image == false && $cpu == false && $memory == false)} {
2131             if {$cpu != false} {
2132                 _log_and_puts log_cpu "Total CPU difference: [_diff_show_ratio $stat(cpu1) $stat(cpu2)]"
2133             } else {
2134                 _log_and_puts log "Total CPU difference: [_diff_show_ratio $stat(cpu1) $stat(cpu2)]"
2135             }
2136         }
2137     }
2138 }
2139
2140 # Auxiliary procedure to save log of results comparison to file
2141 proc _log_html_diff {file log dir1 dir2 highlight_percent} {
2142     # create missing directories as needed
2143     catch {file mkdir [file dirname $file]}
2144
2145     # try to open a file
2146     if [catch {set fd [open $file w]} res] {
2147         error "Error saving log file $file: $res"
2148     }
2149     
2150     # print header
2151     puts $fd "<html><head><title>Diff $dir1 vs. $dir2</title></head><body>"
2152     puts $fd "<h1>Comparison of test results:</h1>"
2153     puts $fd "<h2>Version A - $dir1</h2>"
2154     puts $fd "<h2>Version B - $dir2</h2>"
2155
2156     # print log body, trying to add HTML links to script files on lines like
2157     # "Executing <filename>..."
2158     puts $fd "<pre>"
2159     set logpath [file split [file normalize $file]]
2160     foreach line $log {
2161         # put a line; highlight considerable (> ${highlight_percent}%) deviations of CPU and memory
2162         if { [regexp "\[\\\[](\[0-9.e+-]+)%\[\]]" $line res value] && 
2163              [expr abs($value)] > ${highlight_percent} } {
2164             puts $fd "<table><tr><td bgcolor=\"[expr $value > 0 ? \"ff8080\" : \"lightgreen\"]\">$line</td></tr></table>"
2165         } else {
2166             puts $fd $line
2167         }
2168
2169         # add images
2170         if { [regexp {IMAGE[ \t]+([^:]+):[ \t]+([A-Za-z0-9_.-]+)} $line res case img] } {
2171             if { [catch {eval file join "" [lrange $case 0 end-1]} gridpath] } {
2172                 # note: special handler for the case if test grid directoried are compared directly
2173                 set gridpath ""
2174             }
2175             set img1 "<img src=\"[_make_url $file [file join $dir1 $gridpath $img]]\">"
2176             set img2 "<img src=\"[_make_url $file [file join $dir2 $gridpath $img]]\">"
2177
2178             set difffile [_diff_img_name $dir1 $dir2 $gridpath $img]
2179             if { [file exists $difffile] } {
2180                 set imgd "<img src=\"[_make_url $file $difffile]\">"
2181             } else {
2182                 set imgd "N/A"
2183             }
2184
2185             puts $fd "<table><tr><th><abbr title=\"$dir1\">Version A</abbr></th><th><abbr title=\"$dir2\">Version B</abbr></th><th>Different pixels</th></tr>"
2186             puts $fd "<tr><td>$img1</td><td>$img2</td><td>$imgd</td></tr></table>"
2187         }
2188     }
2189     puts $fd "</pre></body></html>"
2190
2191     close $fd
2192     return
2193 }
2194
2195 # get number of CPUs on the system
2196 proc _get_nb_cpus {} {
2197     global tcl_platform env
2198
2199     if { "$tcl_platform(platform)" == "windows" } {
2200         # on Windows, take the value of the environment variable 
2201         if { [info exists env(NUMBER_OF_PROCESSORS)] &&
2202              ! [catch {expr $env(NUMBER_OF_PROCESSORS) > 0} res] && $res >= 0 } {
2203             return $env(NUMBER_OF_PROCESSORS)
2204         }
2205     } elseif { "$tcl_platform(os)" == "Linux" } {
2206         # on Linux, take number of logical processors listed in /proc/cpuinfo
2207         if { [catch {open "/proc/cpuinfo" r} fd] } { 
2208             return 0 ;# should never happen, but...
2209         }
2210         set nb 0
2211         while { [gets $fd line] >= 0 } {
2212             if { [regexp {^processor[ \t]*:} $line] } {
2213                 incr nb
2214             }
2215         }
2216         close $fd
2217         return $nb
2218     } elseif { "$tcl_platform(os)" == "Darwin" } {
2219         # on MacOS X, call sysctl command
2220         if { ! [catch {exec sysctl hw.ncpu} ret] && 
2221              [regexp {^hw[.]ncpu[ \t]*:[ \t]*([0-9]+)} $ret res nb] } {
2222             return $nb
2223         }
2224     }
2225
2226     # if cannot get good value, return 0 as default
2227     return 0
2228 }
2229
2230 # check two files for difference
2231 proc _diff_files {file1 file2} {
2232     set fd1 [open $file1 "r"]
2233     set fd2 [open $file2 "r"]
2234
2235     set differ f
2236     while {! $differ} {
2237         set nb1 [gets $fd1 line1]
2238         set nb2 [gets $fd2 line2]
2239         if { $nb1 != $nb2 } { set differ t; break }
2240         if { $nb1 < 0 } { break }
2241         if { [string compare $line1 $line2] } {
2242             set differ t
2243         }
2244     }
2245
2246     close $fd1
2247     close $fd2
2248
2249     return $differ
2250 }
2251
2252 # Check if file is in DOS encoding.
2253 # This check is done by presence of \r\n combination at the end of the first 
2254 # line (i.e. prior to any other \n symbol).
2255 # Note that presence of non-ascii symbols typically used for recognition
2256 # of binary files is not suitable since some IGES and STEP files contain
2257 # non-ascii symbols.
2258 # Special check is added for PNG files which contain \r\n in the beginning.
2259 proc _check_dos_encoding {file} {
2260     set fd [open $file rb]
2261     set isdos f
2262     if { [gets $fd line] && [regexp {.*\r$} $line] && 
2263          ! [regexp {^.PNG} $line] } {
2264         set isdos t
2265     }
2266     close $fd
2267     return $isdos
2268 }
2269
2270 # procedure to recognize format of a data file by its first symbols (for OCCT 
2271 # BREP and geometry DRAW formats, IGES, and STEP) and extension (all others)
2272 proc _check_file_format {file} {
2273     set fd [open $file rb]
2274     set line [read $fd 1024]
2275     close $fd
2276
2277     set warn f
2278     set ext [file extension $file]
2279     set format unknown
2280     if { [regexp {^DBRep_DrawableShape} $line] } {
2281         set format BREP
2282         if { "$ext" != ".brep" && "$ext" != ".rle" && 
2283              "$ext" != ".draw" && "$ext" != "" } {
2284             set warn t
2285         }
2286     } elseif { [regexp {^DrawTrSurf_} $line] } {
2287         set format DRAW
2288         if { "$ext" != ".rle" && 
2289              "$ext" != ".draw" && "$ext" != "" } {
2290             set warn t
2291         }
2292     } elseif { [regexp {^[ \t]*ISO-10303-21} $line] } {
2293         set format STEP
2294         if { "$ext" != ".step" && "$ext" != ".stp" } {
2295             set warn t
2296         }
2297     } elseif { [regexp {^.\{72\}S[0 ]\{6\}1} $line] } {
2298         set format IGES
2299         if { "$ext" != ".iges" && "$ext" != ".igs" } {
2300             set warn t
2301         }
2302     } elseif { "$ext" == ".igs" } {
2303         set format IGES
2304     } elseif { "$ext" == ".stp" } {
2305         set format STEP
2306     } else {
2307         set format [string toupper [string range $ext 1 end]]
2308     }
2309     
2310     if { $warn } {
2311         puts "$file: Warning: extension ($ext) does not match format ($format)"
2312     }
2313
2314     return $format
2315 }
2316
2317 # procedure to load file knowing its format
2318 proc load_data_file {file format shape} {
2319     switch $format {
2320         BREP { uplevel restore $file $shape }
2321         DRAW { uplevel restore $file $shape }
2322         IGES { pload XSDRAW; uplevel igesbrep $file $shape * }
2323         STEP { pload XSDRAW; uplevel stepread $file __a *; uplevel renamevar __a_1 $shape }
2324         STL  { pload XSDRAW; uplevel readstl $shape $file }
2325         default { error "Cannot read $format file $file" }
2326     }
2327 }
2328
2329 # procedure to get name of temporary directory,
2330 # ensuring it is existing and writeable 
2331 proc _get_temp_dir {} {
2332     global env tcl_platform
2333
2334     # check typical environment variables 
2335     foreach var {TempDir Temp Tmp} {
2336         # check different case
2337         foreach name [list [string toupper $var] $var [string tolower $var]] {
2338             if { [info exists env($name)] && [file isdirectory $env($name)] &&
2339                  [file writable $env($name)] } {
2340                 return [regsub -all {\\} $env($name) /]
2341             }
2342         }
2343     }
2344
2345     # check platform-specific locations
2346     set fallback tmp
2347     if { "$tcl_platform(platform)" == "windows" } {
2348         set paths "c:/TEMP c:/TMP /TEMP /TMP"
2349         if { [info exists env(HOMEDRIVE)] && [info exists env(HOMEPATH)] } {
2350             set fallback [regsub -all {\\} "$env(HOMEDRIVE)$env(HOMEPATH)/tmp" /]
2351         }
2352     } else {
2353         set paths "/tmp /var/tmp /usr/tmp"
2354         if { [info exists env(HOME)] } {
2355             set fallback "$env(HOME)/tmp"
2356         }
2357     }
2358     foreach dir $paths {
2359         if { [file isdirectory $dir] && [file writable $dir] } {
2360             return $dir
2361         }
2362     }
2363
2364     # fallback case: use subdir /tmp of home or current dir
2365     file mkdir $fallback
2366     return $fallback
2367 }
2368
2369 # extract of code from testgrid command used to process jobs running in 
2370 # parallel until number of jobs in the queue becomes equal or less than 
2371 # specified value
2372 proc _testgrid_process_jobs {worker {nb_ok 0}} {
2373     # bind local vars to variables of the caller procedure
2374     upvar log log
2375     upvar logdir logdir
2376     upvar job_def job_def
2377     upvar nbpooled nbpooled
2378     upvar userbreak userbreak
2379     upvar refresh refresh
2380     upvar refresh_timer refresh_timer
2381
2382     catch {tpool::resume $worker}
2383     while { ! $userbreak && $nbpooled > $nb_ok } {
2384         foreach job [tpool::wait $worker [array names job_def]] {
2385             eval _log_test_case \[tpool::get $worker $job\] $job_def($job) log
2386             unset job_def($job)
2387             incr nbpooled -1
2388         }
2389
2390         # check for user break
2391         if { "[info commands dbreak]" == "dbreak" && [catch dbreak] } {
2392             set userbreak 1
2393         }
2394
2395         # update summary log with requested period
2396         if { $logdir != "" && $refresh > 0 && [clock seconds] > $refresh_timer + $refresh } {
2397             _log_summarize $logdir $log
2398             set refresh_timer [clock seconds]
2399         }
2400     }
2401     catch {tpool::suspend $worker}
2402 }
2403
2404 help checkcolor {
2405   Check pixel color.
2406   Use: checkcolor x y red green blue
2407   x y - pixel coordinates
2408   red green blue - expected pixel color (values from 0 to 1)
2409   Function check color with tolerance (5x5 area)
2410 }
2411 # Procedure to check color using command vreadpixel with tolerance
2412 proc checkcolor { coord_x coord_y rd_get gr_get bl_get } {
2413     puts "Coordinate x = $coord_x"
2414     puts "Coordinate y = $coord_y"
2415     puts "RED color of RGB is $rd_get"
2416     puts "GREEN color of RGB is $gr_get"
2417     puts "BLUE color of RGB is $bl_get"
2418
2419     if { $coord_x <= 1 || $coord_y <= 1 } {
2420         puts "Error : minimal coordinate is x = 2, y = 2. But we have x = $coord_x y = $coord_y"
2421         return -1
2422     }
2423
2424     set color ""
2425     catch { [set color "[vreadpixel ${coord_x} ${coord_y} rgb]"] }
2426     if {"$color" == ""} {
2427         puts "Error : Pixel coordinates (${position_x}; ${position_y}) are out of view"
2428     }
2429     set rd [lindex $color 0]
2430     set gr [lindex $color 1]
2431     set bl [lindex $color 2]
2432     set rd_int [expr int($rd * 1.e+05)]
2433     set gr_int [expr int($gr * 1.e+05)]
2434     set bl_int [expr int($bl * 1.e+05)]
2435     set rd_ch [expr int($rd_get * 1.e+05)]
2436     set gr_ch [expr int($gr_get * 1.e+05)]
2437     set bl_ch [expr int($bl_get * 1.e+05)]
2438
2439     if { $rd_ch != 0 } {
2440         set tol_rd [expr abs($rd_ch - $rd_int)/$rd_ch]
2441     } else {
2442         set tol_rd $rd_int
2443     }
2444     if { $gr_ch != 0 } {
2445         set tol_gr [expr abs($gr_ch - $gr_int)/$gr_ch]
2446     } else {
2447         set tol_gr $gr_int
2448     }
2449     if { $bl_ch != 0 } {
2450         set tol_bl [expr abs($bl_ch - $bl_int)/$bl_ch]
2451     } else {
2452         set tol_bl $bl_int
2453     }
2454
2455     set status 0
2456     if { $tol_rd > 0.2 } {
2457         puts "Warning : RED light of additive color model RGB is invalid"
2458         set status 1
2459     }
2460     if { $tol_gr > 0.2 } {
2461         puts "Warning : GREEN light of additive color model RGB is invalid"
2462         set status 1
2463     }
2464     if { $tol_bl > 0.2 } {
2465         puts "Warning : BLUE light of additive color model RGB is invalid"
2466         set status 1
2467     }
2468
2469     if { $status != 0 } {
2470         puts "Warning : Colors of default coordinate are not equal"
2471     }
2472
2473     global stat
2474     if { $tol_rd > 0.2 || $tol_gr > 0.2 || $tol_bl > 0.2 } {
2475         set info [_checkpoint $coord_x $coord_y $rd_ch $gr_ch $bl_ch]
2476         set stat [lindex $info end]
2477         if { ${stat} != 1 } {
2478             puts "Error : Colors are not equal in default coordinate and in the near coordinates too"
2479             return $stat
2480         } else {
2481             puts "Point with valid color was found"
2482             return $stat
2483         }
2484     } else {
2485         set stat 1
2486     }
2487 }
2488
2489 # Procedure to check color in the point near default coordinate
2490 proc _checkpoint {coord_x coord_y rd_ch gr_ch bl_ch} {
2491     set x_start [expr ${coord_x} - 2]
2492     set y_start [expr ${coord_y} - 2]
2493     set mistake 0
2494     set i 0
2495     while { $mistake != 1 && $i <= 5 } {
2496         set j 0
2497         while { $mistake != 1 && $j <= 5 } {
2498             set position_x [expr ${x_start} + $j]
2499             set position_y [expr ${y_start} + $i]
2500             puts $position_x
2501             puts $position_y
2502
2503             set color ""
2504             catch { [set color "[vreadpixel ${position_x} ${position_y} rgb]"] }
2505             if {"$color" == ""} {
2506                 puts "Warning : Pixel coordinates (${position_x}; ${position_y}) are out of view"
2507                 incr j
2508                 continue
2509             }
2510             set rd [lindex $color 0]
2511             set gr [lindex $color 1]
2512             set bl [lindex $color 2]
2513             set rd_int [expr int($rd * 1.e+05)]
2514             set gr_int [expr int($gr * 1.e+05)]
2515             set bl_int [expr int($bl * 1.e+05)]
2516
2517             if { $rd_ch != 0 } {
2518                 set tol_rd [expr abs($rd_ch - $rd_int)/$rd_ch]
2519             } else {
2520                 set tol_rd $rd_int
2521             }
2522             if { $gr_ch != 0 } {
2523                 set tol_gr [expr abs($gr_ch - $gr_int)/$gr_ch]
2524             } else {
2525                 set tol_gr $gr_int
2526             }
2527             if { $bl_ch != 0 } {
2528                 set tol_bl [expr abs($bl_ch - $bl_int)/$bl_ch]
2529             } else {
2530                 set tol_bl $bl_int
2531             }
2532
2533             if { $tol_rd > 0.2 || $tol_gr > 0.2 || $tol_bl > 0.2 } {
2534                 puts "Warning : Point with true color was not found near default coordinates"
2535                 set mistake 0
2536             } else {
2537                 set mistake 1
2538             }
2539             incr j
2540         }
2541         incr i
2542     }
2543     return $mistake
2544 }
2545
2546 # Procedure to check if sequence of values in listval follows linear trend
2547 # adding the same delta on each step.
2548 #
2549 # The function does statistical estimation of the mean variation of the
2550 # values of the sequence, and dispersion, and returns true only if both 
2551 # dispersion and deviation of the mean from expected delta are within 
2552 # specified tolerance.
2553 #
2554 # If mean variation differs from expected delta on more than two dispersions,
2555 # the check fails and procedure raises error with specified message.
2556 #
2557 # Otherwise the procedure returns false meaning that more iterations are needed.
2558 # Note that false is returned in any case if length of listval is less than 3.
2559 #
2560 # See example of use to check memory leaks in bugs/caf/bug23489
2561 #
2562 proc checktrend {listval delta tolerance message} {
2563     set nbval [llength $listval]
2564     if { $nbval < 3} {
2565         return 0
2566     }
2567
2568     # calculate mean value
2569     set mean 0.
2570     set prev [lindex $listval 0]
2571     foreach val [lrange $listval 1 end] {
2572         set mean [expr $mean + ($val - $prev)]
2573         set prev $val
2574     }
2575     set mean [expr $mean / ($nbval - 1)]
2576
2577     # calculate dispersion
2578     set sigma 0.
2579     set prev [lindex $listval 0]
2580     foreach val [lrange $listval 1 end] {
2581         set d [expr ($val - $prev) - $mean]
2582         set sigma [expr $sigma + $d * $d]
2583         set prev $val
2584     }
2585     set sigma [expr sqrt ($sigma / ($nbval - 2))]
2586
2587     puts "Checking trend: nb = $nbval, mean delta = $mean, sigma = $sigma"
2588
2589     # check if deviation is definitely too big
2590     if { abs ($mean - $delta) > $tolerance + 2. * $sigma } {
2591         puts "Checking trend failed: mean delta per step = $mean, sigma = $sigma, expected delta = $delta"
2592         error "$message"
2593     }
2594
2595     # check if deviation is clearly within a range
2596     return [expr abs ($mean - $delta) <= $sigma && $sigma <= $tolerance]
2597 }