0031038: Draw - adaptations for running tests with CLang address sanitizer
authorabv <abv@opencascade.com>
Sun, 6 Oct 2019 06:33:53 +0000 (09:33 +0300)
committerbugmaster <bugmaster@opencascade.com>
Wed, 6 Nov 2019 16:22:33 +0000 (19:22 +0300)
New optional environment variable CSF_CPULIMIT_FACTOR is introduced, allowing scaling the argument given to DRAW command cpulimit by specified factor.

Command testgrid is extended by two new options:
- -skipped: to re-run tests skipped in specified log
- -skip: to skip specified number of tests in the beginning of test sequence

Temporary Tcl scripts generated by test system are now removed from the test log directories immediately after the test execution, to save disk space.

Advises on use of CLang sanitizers are added in guide "Debugging hints and tips".

dox/dev_guides/debug/debug.md
src/Draw/Draw_BasicCommands.cxx
src/DrawResources/TestCommands.tcl
tests/demo/begin [deleted file]

index 8433e2d..70b511e 100644 (file)
@@ -368,3 +368,53 @@ Each counter has its name shown when the collected statistics are printed.
 In DRAW, use command *dperf* to print all performance statistics.
 
 Note that performance counters are not thread-safe.
+
+@section occt_debug_sanitizers Use of compiler sanitizers
+
+GCC and Clang compilers provide options for instrumenting the code with the tools intended for detection of run-time errors, called sanitizers.
+This section provides some hints for using sanitizers for detecting possible errors in OCCT code.
+
+@subsection occt_debug_sanitizers_linux Linux
+
+Example of configuration steps for Ubuntu:
+
+1. In CMake configuration:
+
+  - Use up-to-date version of the GCC or CLang compiler; make sure that if CMAKE_CXX_COMPILER is set to C++ compiler (e.g. "clang++-6.0") and CMAKE_C_COMPILER is set to C compiler (e.g. "clang-6.0")
+  - Ensure that CMAKE_LINKER is set to the C++ linker bundled with compiler (e.g. clang++-6.0); this is important to avoid linking problems
+  - For building with Address sanitizer, set CMAKE_CXX_FLAGS and CMAKE_C_FLAGS to "-fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls"
+  - For building with Undefined Behavior sanitizer, set CMAKE_CXX_FLAGS and CMAKE_C_FLAGS to "-fsanitize=undefined -fno-omit-frame-pointer -fno-optimize-sibling-calls"
+  - Set CMAKE_BUILD_TYPE to RelWithDebInfo to get more informative stack traces on errors
+
+2. Build as usual (make)
+
+  Be prepared that it works much slower than normal build and consumes more disk space.
+
+3. Before running executable, make sure that "llvm-symbolizer" is in PATH; this is necessary to get human-readable stack traces. The tool must have exactly that name.
+
+  If it is installed in common folder (/usr/bin or similar) with different name, one option is to create a symlink, for instance:
+> sudo ln -s /usr/bin/llvm-symbolizer-6.0 /usr/bin/llvm-symbolizer
+
+  Alternatively, add directory where actual llvm-symbolizer is located (such as /usr/lib/llvm-6.0/bin) to the PATH variable. 
+
+4. Set environment variable to disable memory leaks detection (they seem to be reported for every global variable at exit, not much useful):
+> export ASAN_OPTIONS=detect_leaks=0
+
+5. Set environment variable CSF_CPULIMIT_FACTOR to reasonably large number to increase the time limits for program execution (used by OCCT tests) to compensate the performance penalty introduced by sanitizers:
+> export CSF_CPULIMIT_FACTOR=20
+
+6. When using UBSan, set environment variable UBSAN_OPTIONS to get stack traces:
+> export UBSAN_OPTIONS=print_stacktrace=1
+
+7. Run DRAW and perform tests as usual, keeping in mind that running with sanitizer is much heavier than normal build:
+> ./draw.sh relwithdeb  <br>
+> Draw[]> testgrid -parallel 0
+
+Note that when running tests under sanitizers, behavior may be different.
+Known problems (as of CLang 6.0) are:
+- Software signals (access violation etc.) are not handled
+- Heap memory usage always reports zero
+
+@subsection occt_debug_sanitizers_windows Windows
+
+Though CLang toolset is available in Visual Studio 2015 and newer, sanitizer do not seem to be available out of the box (last tested with VS 2019 16.2.3).
index 4b5e060..b469a06 100644 (file)
@@ -537,13 +537,24 @@ static void *CpuFunc(void* /*threadarg*/)
 }
 #endif
 
-#ifdef _WIN32
-static Standard_Integer cpulimit(Draw_Interpretor&, Standard_Integer n, const char** a)
+// Returns time in seconds defined by the argument string,
+// multiplied by factor defined in environment variable
+// CSF_CPULIMIT_FACTOR (if it exists, 1 otherwise)
+static clock_t GetCpuLimit (const Standard_CString theParam)
 {
-#else
+  clock_t aValue = Draw::Atoi (theParam);
+
+  OSD_Environment aEnv("CSF_CPULIMIT_FACTOR");
+  TCollection_AsciiString aEnvStr = aEnv.Value();
+  if (!aEnvStr.IsEmpty())
+  {
+    aValue *= Draw::Atoi (aEnvStr.ToCString());
+  }
+  return aValue;
+}
+
 static Standard_Integer cpulimit(Draw_Interpretor& di, Standard_Integer n, const char** a)
 {
-#endif
   static int aFirst = 1;
 #ifdef _WIN32
   // Windows specific code
@@ -553,7 +564,7 @@ static Standard_Integer cpulimit(Draw_Interpretor& di, Standard_Integer n, const
   if (n <= 1){
     CPU_LIMIT = RLIM_INFINITY;
   } else {
-    CPU_LIMIT = Draw::Atoi (a[1]);
+    CPU_LIMIT = GetCpuLimit (a[1]);
     Standard_Real anUserSeconds, aSystemSeconds;
     OSD_Chronometer::GetProcessCPU (anUserSeconds, aSystemSeconds);
     CPU_CURRENT = clock_t(anUserSeconds + aSystemSeconds);
@@ -573,7 +584,7 @@ static Standard_Integer cpulimit(Draw_Interpretor& di, Standard_Integer n, const
   if (n <= 1)
     rlp.rlim_cur = RLIM_INFINITY;
   else
-    rlp.rlim_cur = Draw::Atoi(a[1]);
+    rlp.rlim_cur = GetCpuLimit (a[1]);
   CPU_LIMIT = rlp.rlim_cur;
 
   int status;
@@ -597,10 +608,10 @@ static Standard_Integer cpulimit(Draw_Interpretor& di, Standard_Integer n, const
     pthread_create(&cpulimitThread, NULL, CpuFunc, NULL);
   }
 #endif
+  di << "CPU and elapsed time limit set to " << (double)CPU_LIMIT << " seconds";
   return 0;
 }
 
-
 //=======================================================================
 //function : mallochook
 //purpose  : 
index 5363fab..ec5acf5 100644 (file)
@@ -144,7 +144,9 @@ help testgrid {
   -xml filename: write XML report for Jenkins (in JUnit-like format)
   -beep: play sound signal at the end of the tests
   -regress dirname: re-run only a set of tests that have been detected as regressions on some previous run.
+  -skipped dirname: re-run only a set of tests that have been skipped on some previous run.
                     Here "dirname" is path to directory containing results of previous run.
+  -skip N: skip first N tests (useful to restart after abort)
   Groups, grids, and test cases to be executed can be specified by list of file 
   masks, separated by spaces or comma; default is all (*).
 }
@@ -172,7 +174,10 @@ proc testgrid {args} {
     set exc_grid 0
     set exc_case 0
     set regress 0
-    set prev_logdir ""
+    set skipped 0
+    set logdir_regr ""
+    set logdir_skip ""
+    set nbskip 0
     for {set narg 0} {$narg < [llength $args]} {incr narg} {
         set arg [lindex $args $narg]
 
@@ -234,13 +239,29 @@ proc testgrid {args} {
         }
 
         # re-run only a set of tests that have been detected as regressions on some previous run
-        if { $arg == "-regress" } {
+        if { $arg == "-regress" || $arg == "-skipped" } {
             incr narg
             if { $narg < [llength $args] && ! [regexp {^-} [lindex $args $narg]] } {
-                set prev_logdir [lindex $args $narg]
-                set regress 1
+                if { $arg == "-regress" } {
+                    set logdir_regr [file normalize [string trim [lindex $args $narg]]]
+                    set regress 1
+                } else {
+                    set logdir_skip [file normalize [string trim [lindex $args $narg]]]
+                    set skipped 1
+                }
             } else {
-                error "Option -regress requires argument"
+                error "Option $arg requires argument"
+            }
+            continue
+        }
+
+        # skip N first tests
+        if { $arg == "-skip" } {
+            incr narg
+            if { $narg < [llength $args] && [string is integer [lindex $args $narg]] } { 
+                set nbskip [lindex $args $narg]
+            } else {
+                error "Option -skip requires integer argument"
             }
             continue
         }
@@ -303,7 +324,6 @@ proc testgrid {args} {
 
     # check that target log directory is empty or does not exist
     set logdir [file normalize [string trim $logdir]]
-    set prev_logdir [file normalize [string trim $prev_logdir]]
     if { $logdir == "" } {
         # if specified logdir is empty string, generate unique name like 
         # results/<branch>_<timestamp>
@@ -332,7 +352,7 @@ proc testgrid {args} {
     # if option "regress" is given
     set rerun_group_grid_case {}
 
-    if { ${regress} > 0 } {
+    if { ${regress} > 0 || ${skipped} > 0 } {
         if { "${groupmask}" != "*"} {
             lappend rerun_group_grid_case [list $groupmask $gridmask $casemask]
         }
@@ -341,8 +361,8 @@ proc testgrid {args} {
     }
 
     if { ${regress} > 0 } {
-        if { [file exists ${prev_logdir}/tests.log] } {
-            set fd [open ${prev_logdir}/tests.log]
+        if { [file exists ${logdir_regr}/tests.log] } {
+            set fd [open ${logdir_regr}/tests.log]
             while { [gets $fd line] >= 0 } {
                 if {[regexp {CASE ([^\s]+) ([^\s]+) ([^\s]+): FAILED} $line dump group grid casename] ||
                     [regexp {CASE ([^\s]+) ([^\s]+) ([^\s]+): IMPROVEMENT} $line dump group grid casename]} {
@@ -351,7 +371,20 @@ proc testgrid {args} {
             }
             close $fd
         } else {
-            error "Error: file ${prev_logdir}/tests.log is not found, check your input arguments!"
+            error "Error: file ${logdir_regr}/tests.log is not found, check your input arguments!"
+        }
+    }
+    if { ${skipped} > 0 } {
+        if { [file exists ${logdir_skip}/tests.log] } {
+            set fd [open ${logdir_skip}/tests.log]
+            while { [gets $fd line] >= 0 } {
+                if {[regexp {CASE ([^\s]+) ([^\s]+) ([^\s]+): SKIPPED} $line dump group grid casename] } {
+                    lappend rerun_group_grid_case [list $group $grid $casename]
+                }
+            }
+            close $fd
+        } else {
+            error "Error: file ${logdir_skip}/tests.log is not found, check your input arguments!"
         }
     }
 
@@ -501,7 +534,11 @@ proc testgrid {args} {
                             continue
                         }
 
-                        lappend tests_list [list $dir $group $grid $casename $casefile]
+                        if { $nbskip > 0 } {
+                            incr nbskip -1
+                        } else {
+                            lappend tests_list [list $dir $group $grid $casename $casefile]
+                        }
                     }
                 }
             }
@@ -571,7 +608,8 @@ proc testgrid {args} {
         if { $logdir != "" } { set imgdir_cmd "set imagedir $logdir/$group/$grid" }
 
         # prepare command file for running test case in separate instance of DRAW
-        set fd_cmd [open $logdir/$group/$grid/${casename}.tcl w]
+        set file_cmd "$logdir/$group/$grid/${casename}.tcl"
+        set fd_cmd [open $file_cmd w]
         puts $fd_cmd "$imgdir_cmd"
         puts $fd_cmd "set test_image $casename"
         puts $fd_cmd "_run_test $dir $group $grid $casefile t"
@@ -594,7 +632,7 @@ proc testgrid {args} {
         # commant to run DRAW with a command file;
         # note that empty string is passed as standard input to avoid possible 
         # hang-ups due to waiting for stdin of the launching process
-        set command "exec <<{} DRAWEXE -f $logdir/$group/$grid/${casename}.tcl"
+        set command "exec <<{} DRAWEXE -f $file_cmd"
 
         # alternative method to run without temporary file; disabled as it needs too many backslashes
         # else {
@@ -1558,6 +1596,7 @@ proc _log_and_puts {logvar message} {
 proc _log_test_case {output logdir dir group grid casename logvar} {
     upvar $logvar log
     set show_errors 0
+
     # check result and make HTML log
     _check_log $dir $group $grid $casename $show_errors $output summary html_log
     lappend log $summary
@@ -1567,6 +1606,11 @@ proc _log_test_case {output logdir dir group grid casename logvar} {
         _log_html $logdir/$group/$grid/$casename.html $html_log "Test $group $grid $casename"
         _log_save $logdir/$group/$grid/$casename.log "$output\n$summary" "Test $group $grid $casename"
     }
+
+    # remove intermediate command file used to run test
+    if { [file exists $logdir/$group/$grid/${casename}.tcl] } {
+        file delete $logdir/$group/$grid/${casename}.tcl
+    }
 }
 
 # Auxiliary procedure to save log to file
@@ -1710,8 +1754,8 @@ proc _log_html_summary {logdir log totals regressions improvements skipped total
     puts $fd "</table>"
 
     # time stamp and elapsed time info
+    puts $fd "<p>Generated on [clock format [clock seconds] -format {%Y-%m-%d %H:%M:%S}] on [info hostname]\n<p>"
     if { $total_time != "" } { 
-        puts $fd "<p>Generated on [clock format [clock seconds] -format {%Y-%m-%d %H:%M:%S}] on [info hostname]\n<p>"
         puts $fd [join [split $total_time "\n"] "<p>"]
     } else {
         puts $fd "<p>NOTE: This is intermediate summary; the tests are still running! This page will refresh automatically until tests are finished."
diff --git a/tests/demo/begin b/tests/demo/begin
deleted file mode 100755 (executable)
index d664818..0000000
+++ /dev/null
@@ -1 +0,0 @@
-# File : begin