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