0029050: SKIPPED test cases are no listed in header of summary.html
[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 skipped 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 $skipped] {
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     set skipped {Skipped:}
1675     if { [info exists stat] } {
1676         foreach status [lsort [array names stat]] {
1677             lappend totals [list [llength $stat($status)] $status]
1678
1679             # separately count improvements (status starting with IMP), skipped (status starting with SKIP) and regressions (all except IMP, OK, BAD, and SKIP)
1680             if { [regexp -nocase {^IMP} $status] } {
1681                 eval lappend improvements $stat($status)
1682             } elseif { [regexp -nocase {^SKIP} $status] } {
1683                 eval lappend skipped $stat($status)
1684             } elseif { $status != "OK" && ! [regexp -nocase {^BAD} $status] && ! [regexp -nocase {^SKIP} $status] } {
1685                 eval lappend regressions $stat($status)
1686             }
1687         }
1688     }
1689
1690     # if time is specified, add totals
1691     if { $total_time != "" } {
1692         if { [llength $improvements] > 1 } {
1693             _log_and_puts log [join $improvements "\n  "]
1694         }
1695         if { [llength $regressions] > 1 } {
1696             _log_and_puts log [join $regressions "\n  "]
1697         }
1698         if { [llength $skipped] > 1 } {
1699             _log_and_puts log [join $skipped "\n  "]
1700         }
1701         if { [llength $improvements] == 1 && [llength $regressions] == 1 } {
1702             _log_and_puts log "No regressions"
1703         }
1704         _log_and_puts log "Total cases: [join $totals {, }]"
1705         _log_and_puts log $total_time
1706     }
1707
1708     # save log to files
1709     if { $logdir != "" } {
1710         _log_html_summary $logdir $log $totals $regressions $improvements $skipped $total_time
1711         _log_save $logdir/tests.log [join $log "\n"] "Tests summary"
1712     }
1713
1714     return
1715 }
1716
1717 # Internal procedure to generate XML log in JUnit style, for further
1718 # consumption by Jenkins or similar systems.
1719 #
1720 # The output is intended to conform to XML schema supported by Jenkins found at
1721 # 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
1722 #
1723 # The mapping of the fields is inspired by annotated schema of Apache Ant JUnit XML format found at
1724 # http://windyroad.org/dl/Open%20Source/JUnit.xsd
1725 proc _log_xml_summary {logdir filename log include_cout} {
1726     global _test_case_regexp
1727
1728     catch {file mkdir [file dirname $filename]}
1729
1730     # try to open a file and start XML
1731     if [catch {set fd [open $filename w]} res] {
1732         error "Error creating XML summary file $filename: $res"
1733     }
1734     puts $fd "<?xml version='1.0' encoding='utf-8'?>"
1735     puts $fd "<testsuites>"
1736
1737     # prototype for command to generate test suite tag
1738     set time_and_host "timestamp=\"[clock format [clock seconds] -format {%Y-%m-%dT%H:%M:%S}]\" hostname=\"[info hostname]\""
1739     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"}
1740
1741     # sort log and process it line-by-line
1742     set group {}
1743     foreach line [lsort -dictionary $log] {
1744         # check that the line is case report in the form "CASE group grid name: result (explanation)"
1745         if { ! [regexp $_test_case_regexp $line res grp grd casename result message] } {
1746             continue
1747         }
1748         set message [string trim $message " \t\r\n()"]
1749
1750         # start new testsuite for each grid
1751         if { $grp != $group || $grd != $grid } {
1752
1753             # write previous test suite
1754             if [info exists testcases] { eval $cmd_testsuite }
1755
1756             set testcases {}
1757             set nbtests 0
1758             set nberr 0
1759             set nbfail 0
1760             set nbskip 0
1761             set time 0.
1762
1763             set group $grp
1764             set grid $grd
1765         }
1766
1767         incr nbtests
1768  
1769         # parse test log and get its CPU time
1770         set testout {}
1771         set add_cpu {}
1772         if { [catch {set fdlog [open $logdir/$group/$grid/${casename}.log r]} ret] } { 
1773             puts "Error: cannot open $logdir/$group/$grid/${casename}.log: $ret"
1774         } else {
1775             while { [gets $fdlog logline] >= 0 } {
1776                 if { $include_cout } {
1777                     append testout "$logline\n"
1778                 }
1779                 if [regexp -nocase {TOTAL CPU TIME:\s*([\d.]+)\s*sec} $logline res cpu] {
1780                     set add_cpu " time=\"$cpu\""
1781                     set time [expr $time + $cpu]
1782                 }
1783             }
1784             close $fdlog
1785         }
1786         if { ! $include_cout } {
1787             set testout "$line\n"
1788         }
1789
1790         # record test case with its output and status
1791         # Mapping is: SKIPPED, BAD, and OK to OK, all other to failure
1792         append testcases "\n  <testcase name=\"$casename\"$add_cpu status=\"$result\">\n"
1793         append testcases "\n    <system-out>\n$testout    </system-out>"
1794         if { $result != "OK" } {
1795             if { [regexp -nocase {^SKIP} $result] } {
1796                 incr nberr
1797                 append testcases "\n    <error name=\"$result\" message=\"$message\"/>"
1798             } elseif { [regexp -nocase {^BAD} $result] } {
1799                 incr nbskip
1800                 append testcases "\n    <skipped>$message</skipped>"
1801             } else {
1802                 incr nbfail
1803                 append testcases "\n    <failure name=\"$result\" message=\"$message\"/>"
1804             }
1805         }
1806         append testcases "\n  </testcase>"
1807     }
1808
1809     # write last test suite
1810     if [info exists testcases] { eval $cmd_testsuite }
1811
1812     # the end
1813     puts $fd "</testsuites>"
1814     close $fd
1815     return
1816 }
1817
1818 # Auxiliary procedure to split path specification (usually defined by
1819 # environment variable) into list of directories or files
1820 proc _split_path {pathspec} {
1821     global tcl_platform
1822
1823     # first replace all \ (which might occur on Windows) by /  
1824     regsub -all "\\\\" $pathspec "/" pathspec
1825
1826     # split path by platform-specific separator
1827     return [split $pathspec [_path_separator]]
1828 }
1829
1830 # Auxiliary procedure to define platform-specific separator for directories in
1831 # path specification
1832 proc _path_separator {} {
1833     global tcl_platform
1834
1835     # split path by platform-specific separator
1836     if { $tcl_platform(platform) == "windows" } {
1837         return ";"
1838     } else {
1839         return ":"
1840     }
1841 }
1842
1843 # Procedure to make a diff and common of two lists
1844 proc _list_diff {list1 list2 _in1 _in2 _common} {
1845     upvar $_in1 in1
1846     upvar $_in2 in2
1847     upvar $_common common
1848
1849     set in1 {}
1850     set in2 {}
1851     set common {}
1852     foreach item $list1 {
1853         if { [lsearch -exact $list2 $item] >= 0 } {
1854             lappend common $item
1855         } else {
1856             lappend in1 $item
1857         }
1858     }
1859     foreach item $list2 {
1860         if { [lsearch -exact $common $item] < 0 } {
1861             lappend in2 $item
1862         }
1863     }
1864     return
1865 }
1866
1867 # procedure to load a file to Tcl string
1868 proc _read_file {filename} {
1869     set fd [open $filename r]
1870     set result [read -nonewline $fd]
1871     close $fd
1872     return $result
1873 }
1874
1875 # procedure to construct name for the mage diff file
1876 proc _diff_img_name {dir1 dir2 casepath imgfile} {
1877     return [file join $dir1 $casepath "diff-[file tail $dir2]-$imgfile"]
1878 }
1879
1880 # auxiliary procedure to produce string comparing two values
1881 proc _diff_show_ratio {value1 value2} {
1882     if {[expr double ($value2)] == 0.} {
1883         return "$value1 / $value2"
1884     } else {
1885         return "$value1 / $value2 \[[format "%+5.2f%%" [expr 100 * ($value1 - $value2) / double($value2)]]\]"
1886     }
1887 }
1888
1889 # procedure to check cpu user time
1890 proc _check_time {regexp_msg} {
1891     upvar log log
1892     upvar log1 log1
1893     upvar log2 log2
1894     upvar log_cpu log_cpu
1895     upvar cpu cpu
1896     upvar basename basename
1897     upvar casename casename
1898     set time1_list [dict create]
1899     set time2_list [dict create]
1900     set cpu_find UNDEFINED
1901
1902     foreach line1 [split $log1 "\n"] {
1903         if { [regexp "${regexp_msg}" $line1 dump chronometer_name cpu_find] } {
1904             dict set time1_list "${chronometer_name}" "${cpu_find}"
1905         }
1906     }
1907
1908     foreach line2 [split $log2 "\n"] {
1909         if { [regexp "${regexp_msg}" $line2 dump chronometer_name cpu_find] } {
1910             dict set time2_list "${chronometer_name}" "${cpu_find}"
1911         }
1912     }
1913
1914     if { [llength [dict keys $time1_list]] != [llength [dict keys $time2_list]] } {
1915         puts "Error: number of dchrono/chrono COUNTER are different in the same test cases"
1916     } else {
1917         foreach key [dict keys $time1_list] {
1918             set time1 [dict get $time1_list $key]
1919             set time2 [dict get $time2_list $key]
1920
1921             # compare CPU user time with 10% precision (but not less 0.5 sec)
1922             if { [expr abs ($time1 - $time2) > 0.5 + 0.05 * abs ($time1 + $time2)] } {
1923                 if {$cpu != false} {
1924                     _log_and_puts log_cpu "COUNTER $key: [split $basename /] $casename: [_diff_show_ratio $time1 $time2]"
1925                 } else {
1926                     _log_and_puts log "COUNTER $key: [split $basename /] $casename: [_diff_show_ratio $time1 $time2]"
1927                 }
1928             }
1929         }
1930     }
1931 }
1932
1933 # Procedure to compare results of two runs of test cases
1934 proc _test_diff {dir1 dir2 basename image cpu memory status verbose _logvar _logimage _logcpu _logmemory {_statvar ""}} {
1935     upvar $_logvar log
1936     upvar $_logimage log_image
1937     upvar $_logcpu log_cpu
1938     upvar $_logmemory log_memory
1939
1940     # make sure to load diffimage command
1941     uplevel pload VISUALIZATION
1942
1943     # prepare variable (array) for collecting statistics
1944     if { "$_statvar" != "" } {
1945         upvar $_statvar stat
1946     } else {
1947         set stat(cpu1) 0
1948         set stat(cpu2) 0
1949         set stat(mem1) 0
1950         set stat(mem2) 0
1951         set log {}
1952         set log_image {}
1953         set log_cpu {}
1954         set log_memory {}
1955     }
1956
1957     # first check subdirectories
1958     set path1 [file join $dir1 $basename]
1959     set path2 [file join $dir2 $basename]
1960     set list1 [glob -directory $path1 -types d -tails -nocomplain *]
1961     set list2 [glob -directory $path2 -types d -tails -nocomplain *]
1962     if { [llength $list1] >0 || [llength $list2] > 0 } {
1963         _list_diff $list1 $list2 in1 in2 common
1964         if { "$verbose" > 1 } {
1965             if { [llength $in1] > 0 } { _log_and_puts log "Only in $path1: $in1" }
1966             if { [llength $in2] > 0 } { _log_and_puts log "Only in $path2: $in2" }
1967         }
1968         foreach subdir $common {
1969             if { "$verbose" > 2 } {
1970                 _log_and_puts log "Checking [file join $basename $subdir]"
1971             }
1972             _test_diff $dir1 $dir2 [file join $basename $subdir] $image $cpu $memory $status $verbose log log_image log_cpu log_memory stat
1973         }
1974     } else {
1975         # check log files (only if directory has no subdirs)
1976         set list1 [glob -directory $path1 -types f -tails -nocomplain *.log]
1977         set list2 [glob -directory $path2 -types f -tails -nocomplain *.log]
1978         _list_diff $list1 $list2 in1 in2 common
1979         if { "$verbose" > 1 } {
1980             if { [llength $in1] > 0 } { _log_and_puts log "Only in $path1: $in1" }
1981             if { [llength $in2] > 0 } { _log_and_puts log "Only in $path2: $in2" }
1982         }
1983         set gcpu1 0
1984         set gcpu2 0
1985         set gmem1 0
1986         set gmem2 0
1987         foreach logfile $common {
1988             # load two logs
1989             set log1 [_read_file [file join $dir1 $basename $logfile]]
1990             set log2 [_read_file [file join $dir2 $basename $logfile]]
1991             set casename [file rootname $logfile]
1992             
1993             # check execution statuses
1994             if {$image == false && $cpu == false && $memory == false} {
1995                 set status1 UNDEFINED
1996                 set status2 UNDEFINED
1997                 if { ! [regexp {CASE [^:]*:\s*([\w]+)} $log1 res1 status1] ||
1998                     ! [regexp {CASE [^:]*:\s*([\w]+)} $log2 res2 status2] ||
1999                     "$status1" != "$status2" } {
2000                     _log_and_puts log "STATUS [split $basename /] $casename: $status1 / $status2"
2001                     # if test statuses are different, further comparison makes 
2002                     # no sense unless explicitly requested
2003                     if { "$status" != "all" } {
2004                         continue
2005                     }
2006                 }
2007                 if { "$status" == "ok" && "$status1" != "OK" } { 
2008                     continue
2009                 }
2010             }
2011
2012             if { ! $image } {
2013                 # check CPU user time in test cases
2014                 set checkCPURegexp "COUNTER (.+): (\[-0-9.+eE\]+)"
2015                 if { [regexp "${checkCPURegexp}" $log1] &&
2016                      [regexp "${checkCPURegexp}" $log2] } {
2017                   _check_time "${checkCPURegexp}"
2018                 }
2019             }
2020             
2021             # check CPU times
2022             if {$cpu != false || ($image == false && $cpu == false && $memory == false)} {
2023                 set cpu1 UNDEFINED
2024                 set cpu2 UNDEFINED
2025                 if { [regexp {TOTAL CPU TIME:\s*([\d.]+)} $log1 res1 cpu1] &&
2026                      [regexp {TOTAL CPU TIME:\s*([\d.]+)} $log2 res1 cpu2] } {
2027                     set stat(cpu1) [expr $stat(cpu1) + $cpu1]
2028                     set stat(cpu2) [expr $stat(cpu2) + $cpu2]
2029                     set gcpu1 [expr $gcpu1 + $cpu1]
2030                     set gcpu2 [expr $gcpu2 + $cpu2]
2031
2032                     # compare CPU times with 10% precision (but not less 0.5 sec)
2033                     if { [expr abs ($cpu1 - $cpu2) > 0.5 + 0.05 * abs ($cpu1 + $cpu2)] } {
2034                         if {$cpu != false} {
2035                             _log_and_puts log_cpu "CPU [split $basename /] $casename: [_diff_show_ratio $cpu1 $cpu2]"
2036                         } else {
2037                             _log_and_puts log "CPU [split $basename /] $casename: [_diff_show_ratio $cpu1 $cpu2]"
2038                         }
2039                     }
2040                 }
2041             }
2042
2043             # check memory delta
2044             if {$memory != false || ($image == false && $cpu == false && $memory == false)} {
2045                 set mem1 UNDEFINED
2046                 set mem2 UNDEFINED
2047                 if { [regexp {MEMORY DELTA:\s*([\d.]+)} $log1 res1 mem1] &&
2048                      [regexp {MEMORY DELTA:\s*([\d.]+)} $log2 res1 mem2] } {
2049                     set stat(mem1) [expr $stat(mem1) + $mem1]
2050                     set stat(mem2) [expr $stat(mem2) + $mem2]
2051                     set gmem1 [expr $gmem1 + $mem1]
2052                     set gmem2 [expr $gmem2 + $mem2]
2053
2054                     # compare memory usage with 10% precision (but not less 16 KiB)
2055                     if { [expr abs ($mem1 - $mem2) > 16 + 0.05 * abs ($mem1 + $mem2)] } {
2056                         if {$memory != false} {
2057                             _log_and_puts log_memory "MEMORY [split $basename /] $casename: [_diff_show_ratio $mem1 $mem2]"
2058                         } else {
2059                             _log_and_puts log "MEMORY [split $basename /] $casename: [_diff_show_ratio $mem1 $mem2]"
2060                         }
2061                     }
2062                 }
2063             }
2064
2065             # check images
2066             if {$image != false || ($image == false && $cpu == false && $memory == false)} {
2067                 set imglist1 [glob -directory $path1 -types f -tails -nocomplain ${casename}.{png,gif} ${casename}-*.{png,gif} ${casename}_*.{png,gif}]
2068                 set imglist2 [glob -directory $path2 -types f -tails -nocomplain ${casename}.{png,gif} ${casename}-*.{png,gif} ${casename}_*.{png,gif}]
2069                 _list_diff $imglist1 $imglist2 imgin1 imgin2 imgcommon
2070                 if { "$verbose" > 1 } {
2071                     if { [llength $imgin1] > 0 } {
2072                         if {$image != false} {
2073                             _log_and_puts log_image "Only in $path1: $imgin1"
2074                         } else {
2075                             _log_and_puts log "Only in $path1: $imgin1"
2076                         }
2077                     }
2078                     if { [llength $imgin2] > 0 } {
2079                         if {$image != false} {
2080                             _log_and_puts log_image "Only in $path2: $imgin2"
2081                         } else {
2082                             _log_and_puts log "Only in $path2: $imgin2"
2083                         }
2084                     }
2085                 }
2086                 foreach imgfile $imgcommon {
2087                     # if { $verbose > 1 } { _log_and_puts log "Checking [split basename /] $casename: $imgfile" }
2088                     set diffile [_diff_img_name $dir1 $dir2 $basename $imgfile]
2089                     if { [catch {diffimage [file join $dir1 $basename $imgfile] \
2090                                            [file join $dir2 $basename $imgfile] \
2091                                            0 0 0 $diffile} diff] } {
2092                         if {$image != false} {
2093                             _log_and_puts log_image "IMAGE [split $basename /] $casename: $imgfile cannot be compared"
2094                         } else {
2095                             _log_and_puts log "IMAGE [split $basename /] $casename: $imgfile cannot be compared"
2096                         }
2097                         file delete -force $diffile ;# clean possible previous result of diffimage
2098                     } elseif { $diff != 0 } {
2099                         if {$image != false} {
2100                             _log_and_puts log_image "IMAGE [split $basename /] $casename: $imgfile differs"
2101                         } else {
2102                             _log_and_puts log "IMAGE [split $basename /] $casename: $imgfile differs"
2103                         }
2104                     } else {
2105                         file delete -force $diffile ;# clean useless artifact of diffimage
2106                     }
2107                 }
2108             }
2109         }
2110         
2111         # report CPU and memory difference in group if it is greater than 10%
2112         if {$cpu != false || ($image == false && $cpu == false && $memory == false)} {
2113             if { [expr abs ($gcpu1 - $gcpu2) > 0.5 + 0.005 * abs ($gcpu1 + $gcpu2)] } {
2114                 if {$cpu != false} {
2115                     _log_and_puts log_cpu "CPU [split $basename /]: [_diff_show_ratio $gcpu1 $gcpu2]"
2116                 } else {
2117                     _log_and_puts log "CPU [split $basename /]: [_diff_show_ratio $gcpu1 $gcpu2]"
2118                 }
2119             }
2120         }
2121         if {$memory != false || ($image == false && $cpu == false && $memory == false)} {
2122             if { [expr abs ($gmem1 - $gmem2) > 16 + 0.005 * abs ($gmem1 + $gmem2)] } {
2123                 if {$memory != false} {
2124                     _log_and_puts log_memory "MEMORY [split $basename /]: [_diff_show_ratio $gmem1 $gmem2]"
2125                 } else {
2126                     _log_and_puts log "MEMORY [split $basename /]: [_diff_show_ratio $gmem1 $gmem2]"
2127                 }
2128             }
2129         }
2130     }
2131
2132     if { "$_statvar" == "" } {
2133         if {$memory != false || ($image == false && $cpu == false && $memory == false)} {
2134             if {$memory != false} {
2135                 _log_and_puts log_memory "Total MEMORY difference: [_diff_show_ratio $stat(mem1) $stat(mem2)]"
2136             } else {
2137                 _log_and_puts log "Total MEMORY difference: [_diff_show_ratio $stat(mem1) $stat(mem2)]"
2138             }
2139         }
2140         if {$cpu != false || ($image == false && $cpu == false && $memory == false)} {
2141             if {$cpu != false} {
2142                 _log_and_puts log_cpu "Total CPU difference: [_diff_show_ratio $stat(cpu1) $stat(cpu2)]"
2143             } else {
2144                 _log_and_puts log "Total CPU difference: [_diff_show_ratio $stat(cpu1) $stat(cpu2)]"
2145             }
2146         }
2147     }
2148 }
2149
2150 # Auxiliary procedure to save log of results comparison to file
2151 proc _log_html_diff {file log dir1 dir2 highlight_percent} {
2152     # create missing directories as needed
2153     catch {file mkdir [file dirname $file]}
2154
2155     # try to open a file
2156     if [catch {set fd [open $file w]} res] {
2157         error "Error saving log file $file: $res"
2158     }
2159     
2160     # print header
2161     puts $fd "<html><head><title>Diff $dir1 vs. $dir2</title></head><body>"
2162     puts $fd "<h1>Comparison of test results:</h1>"
2163     puts $fd "<h2>Version A - $dir1</h2>"
2164     puts $fd "<h2>Version B - $dir2</h2>"
2165
2166     # add script for switching between images on click
2167     puts $fd ""
2168     puts $fd "<script type=\"text/javascript\">"
2169     puts $fd "  function diffimage_toggle(img,url1,url2)"
2170     puts $fd "  {"
2171     puts $fd "    if (img.show2nd) { img.src = url1; img.show2nd = false; }"
2172     puts $fd "    else { img.src = url2; img.show2nd = true; }"
2173     puts $fd "  }"
2174     puts $fd "  function diffimage_reset(img,url) { img.src = url; img.show2nd = true; }"
2175     puts $fd "</script>"
2176     puts $fd ""
2177
2178     # print log body
2179     puts $fd "<pre>"
2180     set logpath [file split [file normalize $file]]
2181     foreach line $log {
2182         # put a line; highlight considerable (> ${highlight_percent}%) deviations of CPU and memory
2183         if { [regexp "\[\\\[](\[0-9.e+-]+)%\[\]]" $line res value] && 
2184              [expr abs($value)] > ${highlight_percent} } {
2185             puts $fd "<table><tr><td bgcolor=\"[expr $value > 0 ? \"ff8080\" : \"lightgreen\"]\">$line</td></tr></table>"
2186         } else {
2187             puts $fd $line
2188         }
2189
2190         # add images
2191         if { [regexp {IMAGE[ \t]+([^:]+):[ \t]+([A-Za-z0-9_.-]+)} $line res case img] } {
2192             if { [catch {eval file join "" [lrange $case 0 end-1]} gridpath] } {
2193                 # note: special handler for the case if test grid directoried are compared directly
2194                 set gridpath ""
2195             }
2196             set aCaseName [lindex $case end]
2197             set img1url [_make_url $file [file join $dir1 $gridpath $img]]
2198             set img2url [_make_url $file [file join $dir2 $gridpath $img]]
2199             set img1 "<a href=\"[_make_url $file [file join $dir1 $gridpath $aCaseName.html]]\"><img src=\"$img1url\"></a>"
2200             set img2 "<a href=\"[_make_url $file [file join $dir2 $gridpath $aCaseName.html]]\"><img src=\"$img2url\"></a>"
2201
2202             set difffile [_diff_img_name $dir1 $dir2 $gridpath $img]
2203             set imgdurl [_make_url $file $difffile]
2204             if { [file exists $difffile] } {
2205                 set imgd "<img src=\"$imgdurl\" onmouseout=diffimage_reset(this,\"$imgdurl\") onclick=diffimage_toggle(this,\"$img1url\",\"$img2url\")>"
2206             } else {
2207                 set imgd "N/A"
2208             }
2209
2210             puts $fd "<table><tr><th><abbr title=\"$dir1\">Version A</abbr></th><th><abbr title=\"$dir2\">Version B</abbr></th><th>Diff (click to toggle)</th></tr>"
2211             puts $fd "<tr><td>$img1</td><td>$img2</td><td>$imgd</td></tr></table>"
2212         }
2213     }
2214     puts $fd "</pre></body></html>"
2215
2216     close $fd
2217     return
2218 }
2219
2220 # get number of CPUs on the system
2221 proc _get_nb_cpus {} {
2222     global tcl_platform env
2223
2224     if { "$tcl_platform(platform)" == "windows" } {
2225         # on Windows, take the value of the environment variable 
2226         if { [info exists env(NUMBER_OF_PROCESSORS)] &&
2227              ! [catch {expr $env(NUMBER_OF_PROCESSORS) > 0} res] && $res >= 0 } {
2228             return $env(NUMBER_OF_PROCESSORS)
2229         }
2230     } elseif { "$tcl_platform(os)" == "Linux" } {
2231         # on Linux, take number of logical processors listed in /proc/cpuinfo
2232         if { [catch {open "/proc/cpuinfo" r} fd] } { 
2233             return 0 ;# should never happen, but...
2234         }
2235         set nb 0
2236         while { [gets $fd line] >= 0 } {
2237             if { [regexp {^processor[ \t]*:} $line] } {
2238                 incr nb
2239             }
2240         }
2241         close $fd
2242         return $nb
2243     } elseif { "$tcl_platform(os)" == "Darwin" } {
2244         # on MacOS X, call sysctl command
2245         if { ! [catch {exec sysctl hw.ncpu} ret] && 
2246              [regexp {^hw[.]ncpu[ \t]*:[ \t]*([0-9]+)} $ret res nb] } {
2247             return $nb
2248         }
2249     }
2250
2251     # if cannot get good value, return 0 as default
2252     return 0
2253 }
2254
2255 # check two files for difference
2256 proc _diff_files {file1 file2} {
2257     set fd1 [open $file1 "r"]
2258     set fd2 [open $file2 "r"]
2259
2260     set differ f
2261     while {! $differ} {
2262         set nb1 [gets $fd1 line1]
2263         set nb2 [gets $fd2 line2]
2264         if { $nb1 != $nb2 } { set differ t; break }
2265         if { $nb1 < 0 } { break }
2266         if { [string compare $line1 $line2] } {
2267             set differ t
2268         }
2269     }
2270
2271     close $fd1
2272     close $fd2
2273
2274     return $differ
2275 }
2276
2277 # Check if file is in DOS encoding.
2278 # This check is done by presence of \r\n combination at the end of the first 
2279 # line (i.e. prior to any other \n symbol).
2280 # Note that presence of non-ascii symbols typically used for recognition
2281 # of binary files is not suitable since some IGES and STEP files contain
2282 # non-ascii symbols.
2283 # Special check is added for PNG files which contain \r\n in the beginning.
2284 proc _check_dos_encoding {file} {
2285     set fd [open $file rb]
2286     set isdos f
2287     if { [gets $fd line] && [regexp {.*\r$} $line] && 
2288          ! [regexp {^.PNG} $line] } {
2289         set isdos t
2290     }
2291     close $fd
2292     return $isdos
2293 }
2294
2295 # procedure to recognize format of a data file by its first symbols (for OCCT 
2296 # BREP and geometry DRAW formats, IGES, and STEP) and extension (all others)
2297 proc _check_file_format {file} {
2298     set fd [open $file rb]
2299     set line [read $fd 1024]
2300     close $fd
2301
2302     set warn f
2303     set ext [file extension $file]
2304     set format unknown
2305     if { [regexp {^DBRep_DrawableShape} $line] } {
2306         set format BREP
2307         if { "$ext" != ".brep" && "$ext" != ".rle" && 
2308              "$ext" != ".draw" && "$ext" != "" } {
2309             set warn t
2310         }
2311     } elseif { [regexp {^DrawTrSurf_} $line] } {
2312         set format DRAW
2313         if { "$ext" != ".rle" && 
2314              "$ext" != ".draw" && "$ext" != "" } {
2315             set warn t
2316         }
2317     } elseif { [regexp {^[ \t]*ISO-10303-21} $line] } {
2318         set format STEP
2319         if { "$ext" != ".step" && "$ext" != ".stp" } {
2320             set warn t
2321         }
2322     } elseif { [regexp {^.\{72\}S[0 ]\{6\}1} $line] } {
2323         set format IGES
2324         if { "$ext" != ".iges" && "$ext" != ".igs" } {
2325             set warn t
2326         }
2327     } elseif { "$ext" == ".igs" } {
2328         set format IGES
2329     } elseif { "$ext" == ".stp" } {
2330         set format STEP
2331     } else {
2332         set format [string toupper [string range $ext 1 end]]
2333     }
2334     
2335     if { $warn } {
2336         puts "$file: Warning: extension ($ext) does not match format ($format)"
2337     }
2338
2339     return $format
2340 }
2341
2342 # procedure to load file knowing its format
2343 proc load_data_file {file format shape} {
2344     switch $format {
2345         BREP { uplevel restore $file $shape }
2346         DRAW { uplevel restore $file $shape }
2347         IGES { pload XSDRAW; uplevel igesbrep $file $shape * }
2348         STEP { pload XSDRAW; uplevel stepread $file __a *; uplevel renamevar __a_1 $shape }
2349         STL  { pload XSDRAW; uplevel readstl $shape $file }
2350         default { error "Cannot read $format file $file" }
2351     }
2352 }
2353
2354 # procedure to get name of temporary directory,
2355 # ensuring it is existing and writeable 
2356 proc _get_temp_dir {} {
2357     global env tcl_platform
2358
2359     # check typical environment variables 
2360     foreach var {TempDir Temp Tmp} {
2361         # check different case
2362         foreach name [list [string toupper $var] $var [string tolower $var]] {
2363             if { [info exists env($name)] && [file isdirectory $env($name)] &&
2364                  [file writable $env($name)] } {
2365                 return [regsub -all {\\} $env($name) /]
2366             }
2367         }
2368     }
2369
2370     # check platform-specific locations
2371     set fallback tmp
2372     if { "$tcl_platform(platform)" == "windows" } {
2373         set paths "c:/TEMP c:/TMP /TEMP /TMP"
2374         if { [info exists env(HOMEDRIVE)] && [info exists env(HOMEPATH)] } {
2375             set fallback [regsub -all {\\} "$env(HOMEDRIVE)$env(HOMEPATH)/tmp" /]
2376         }
2377     } else {
2378         set paths "/tmp /var/tmp /usr/tmp"
2379         if { [info exists env(HOME)] } {
2380             set fallback "$env(HOME)/tmp"
2381         }
2382     }
2383     foreach dir $paths {
2384         if { [file isdirectory $dir] && [file writable $dir] } {
2385             return $dir
2386         }
2387     }
2388
2389     # fallback case: use subdir /tmp of home or current dir
2390     file mkdir $fallback
2391     return $fallback
2392 }
2393
2394 # extract of code from testgrid command used to process jobs running in 
2395 # parallel until number of jobs in the queue becomes equal or less than 
2396 # specified value
2397 proc _testgrid_process_jobs {worker {nb_ok 0}} {
2398     # bind local vars to variables of the caller procedure
2399     upvar log log
2400     upvar logdir logdir
2401     upvar job_def job_def
2402     upvar nbpooled nbpooled
2403     upvar userbreak userbreak
2404     upvar refresh refresh
2405     upvar refresh_timer refresh_timer
2406
2407     catch {tpool::resume $worker}
2408     while { ! $userbreak && $nbpooled > $nb_ok } {
2409         foreach job [tpool::wait $worker [array names job_def]] {
2410             eval _log_test_case \[tpool::get $worker $job\] $job_def($job) log
2411             unset job_def($job)
2412             incr nbpooled -1
2413         }
2414
2415         # check for user break
2416         if { "[info commands dbreak]" == "dbreak" && [catch dbreak] } {
2417             set userbreak 1
2418         }
2419
2420         # update summary log with requested period
2421         if { $logdir != "" && $refresh > 0 && [clock seconds] > $refresh_timer + $refresh } {
2422             _log_summarize $logdir $log
2423             set refresh_timer [clock seconds]
2424         }
2425     }
2426     catch {tpool::suspend $worker}
2427 }
2428
2429 help checkcolor {
2430   Check pixel color.
2431   Use: checkcolor x y red green blue
2432   x y - pixel coordinates
2433   red green blue - expected pixel color (values from 0 to 1)
2434   Function check color with tolerance (5x5 area)
2435 }
2436 # Procedure to check color using command vreadpixel with tolerance
2437 proc checkcolor { coord_x coord_y rd_get gr_get bl_get } {
2438     puts "Coordinate x = $coord_x"
2439     puts "Coordinate y = $coord_y"
2440     puts "RED color of RGB is $rd_get"
2441     puts "GREEN color of RGB is $gr_get"
2442     puts "BLUE color of RGB is $bl_get"
2443
2444     if { $coord_x <= 1 || $coord_y <= 1 } {
2445         puts "Error : minimal coordinate is x = 2, y = 2. But we have x = $coord_x y = $coord_y"
2446         return -1
2447     }
2448
2449     set color ""
2450     catch { [set color "[vreadpixel ${coord_x} ${coord_y} rgb]"] }
2451     if {"$color" == ""} {
2452         puts "Error : Pixel coordinates (${position_x}; ${position_y}) are out of view"
2453     }
2454     set rd [lindex $color 0]
2455     set gr [lindex $color 1]
2456     set bl [lindex $color 2]
2457     set rd_int [expr int($rd * 1.e+05)]
2458     set gr_int [expr int($gr * 1.e+05)]
2459     set bl_int [expr int($bl * 1.e+05)]
2460     set rd_ch [expr int($rd_get * 1.e+05)]
2461     set gr_ch [expr int($gr_get * 1.e+05)]
2462     set bl_ch [expr int($bl_get * 1.e+05)]
2463
2464     if { $rd_ch != 0 } {
2465         set tol_rd [expr abs($rd_ch - $rd_int)/$rd_ch]
2466     } else {
2467         set tol_rd $rd_int
2468     }
2469     if { $gr_ch != 0 } {
2470         set tol_gr [expr abs($gr_ch - $gr_int)/$gr_ch]
2471     } else {
2472         set tol_gr $gr_int
2473     }
2474     if { $bl_ch != 0 } {
2475         set tol_bl [expr abs($bl_ch - $bl_int)/$bl_ch]
2476     } else {
2477         set tol_bl $bl_int
2478     }
2479
2480     set status 0
2481     if { $tol_rd > 0.2 } {
2482         puts "Warning : RED light of additive color model RGB is invalid"
2483         set status 1
2484     }
2485     if { $tol_gr > 0.2 } {
2486         puts "Warning : GREEN light of additive color model RGB is invalid"
2487         set status 1
2488     }
2489     if { $tol_bl > 0.2 } {
2490         puts "Warning : BLUE light of additive color model RGB is invalid"
2491         set status 1
2492     }
2493
2494     if { $status != 0 } {
2495         puts "Warning : Colors of default coordinate are not equal"
2496     }
2497
2498     global stat
2499     if { $tol_rd > 0.2 || $tol_gr > 0.2 || $tol_bl > 0.2 } {
2500         set info [_checkpoint $coord_x $coord_y $rd_ch $gr_ch $bl_ch]
2501         set stat [lindex $info end]
2502         if { ${stat} != 1 } {
2503             puts "Error : Colors are not equal in default coordinate and in the near coordinates too"
2504             return $stat
2505         } else {
2506             puts "Point with valid color was found"
2507             return $stat
2508         }
2509     } else {
2510         set stat 1
2511     }
2512 }
2513
2514 # Procedure to check color in the point near default coordinate
2515 proc _checkpoint {coord_x coord_y rd_ch gr_ch bl_ch} {
2516     set x_start [expr ${coord_x} - 2]
2517     set y_start [expr ${coord_y} - 2]
2518     set mistake 0
2519     set i 0
2520     while { $mistake != 1 && $i <= 5 } {
2521         set j 0
2522         while { $mistake != 1 && $j <= 5 } {
2523             set position_x [expr ${x_start} + $j]
2524             set position_y [expr ${y_start} + $i]
2525             puts $position_x
2526             puts $position_y
2527
2528             set color ""
2529             catch { [set color "[vreadpixel ${position_x} ${position_y} rgb]"] }
2530             if {"$color" == ""} {
2531                 puts "Warning : Pixel coordinates (${position_x}; ${position_y}) are out of view"
2532                 incr j
2533                 continue
2534             }
2535             set rd [lindex $color 0]
2536             set gr [lindex $color 1]
2537             set bl [lindex $color 2]
2538             set rd_int [expr int($rd * 1.e+05)]
2539             set gr_int [expr int($gr * 1.e+05)]
2540             set bl_int [expr int($bl * 1.e+05)]
2541
2542             if { $rd_ch != 0 } {
2543                 set tol_rd [expr abs($rd_ch - $rd_int)/$rd_ch]
2544             } else {
2545                 set tol_rd $rd_int
2546             }
2547             if { $gr_ch != 0 } {
2548                 set tol_gr [expr abs($gr_ch - $gr_int)/$gr_ch]
2549             } else {
2550                 set tol_gr $gr_int
2551             }
2552             if { $bl_ch != 0 } {
2553                 set tol_bl [expr abs($bl_ch - $bl_int)/$bl_ch]
2554             } else {
2555                 set tol_bl $bl_int
2556             }
2557
2558             if { $tol_rd > 0.2 || $tol_gr > 0.2 || $tol_bl > 0.2 } {
2559                 puts "Warning : Point with true color was not found near default coordinates"
2560                 set mistake 0
2561             } else {
2562                 set mistake 1
2563             }
2564             incr j
2565         }
2566         incr i
2567     }
2568     return $mistake
2569 }
2570
2571 # Procedure to check if sequence of values in listval follows linear trend
2572 # adding the same delta on each step.
2573 #
2574 # The function does statistical estimation of the mean variation of the
2575 # values of the sequence, and dispersion, and returns true only if both 
2576 # dispersion and deviation of the mean from expected delta are within 
2577 # specified tolerance.
2578 #
2579 # If mean variation differs from expected delta on more than two dispersions,
2580 # the check fails and procedure raises error with specified message.
2581 #
2582 # Otherwise the procedure returns false meaning that more iterations are needed.
2583 # Note that false is returned in any case if length of listval is less than 3.
2584 #
2585 # See example of use to check memory leaks in bugs/caf/bug23489
2586 #
2587 proc checktrend {listval delta tolerance message} {
2588     set nbval [llength $listval]
2589     if { $nbval < 3} {
2590         return 0
2591     }
2592
2593     # calculate mean value
2594     set mean 0.
2595     set prev [lindex $listval 0]
2596     foreach val [lrange $listval 1 end] {
2597         set mean [expr $mean + ($val - $prev)]
2598         set prev $val
2599     }
2600     set mean [expr $mean / ($nbval - 1)]
2601
2602     # calculate dispersion
2603     set sigma 0.
2604     set prev [lindex $listval 0]
2605     foreach val [lrange $listval 1 end] {
2606         set d [expr ($val - $prev) - $mean]
2607         set sigma [expr $sigma + $d * $d]
2608         set prev $val
2609     }
2610     set sigma [expr sqrt ($sigma / ($nbval - 2))]
2611
2612     puts "Checking trend: nb = $nbval, mean delta = $mean, sigma = $sigma"
2613
2614     # check if deviation is definitely too big
2615     if { abs ($mean - $delta) > $tolerance + 2. * $sigma } {
2616         puts "Checking trend failed: mean delta per step = $mean, sigma = $sigma, expected delta = $delta"
2617         error "$message"
2618     }
2619
2620     # check if deviation is clearly within a range
2621     return [expr abs ($mean - $delta) <= $sigma && $sigma <= $tolerance]
2622 }