Tests: fail automated tests on memory leaks and other internal errors

This adds a new `--debug-exit-on-error` flag. When it is set, Blender
will abort with a non-zero exit code when there are internal errors.
Currently, "internal errors" includes memory leaks detected by
guardedalloc and error/fatal log entries in clog.

The new flag is passed to Blender in various places where automated
tests are run. Furthermore, the `--debug-memory` flag is used in tests,
because that makes the verbose output more useful, when dealing
with memory leaks.

Reviewers: brecht, sergey

Differential Revision: https://developer.blender.org/D8665
This commit is contained in:
Jacques Lucke 2020-08-26 22:02:02 +02:00
parent d8cf6ee316
commit 8a9912eaf8
15 changed files with 90 additions and 10 deletions

@ -139,6 +139,7 @@ void CLG_exit(void);
void CLG_output_set(void *file_handle); void CLG_output_set(void *file_handle);
void CLG_output_use_basename_set(int value); void CLG_output_use_basename_set(int value);
void CLG_output_use_timestamp_set(int value); void CLG_output_use_timestamp_set(int value);
void CLG_error_fn_set(void (*error_fn)(void *file_handle));
void CLG_fatal_fn_set(void (*fatal_fn)(void *file_handle)); void CLG_fatal_fn_set(void (*fatal_fn)(void *file_handle));
void CLG_backtrace_fn_set(void (*fatal_fn)(void *file_handle)); void CLG_backtrace_fn_set(void (*fatal_fn)(void *file_handle));

@ -98,6 +98,7 @@ typedef struct CLogContext {
} default_type; } default_type;
struct { struct {
void (*error_fn)(void *file_handle);
void (*fatal_fn)(void *file_handle); void (*fatal_fn)(void *file_handle);
void (*backtrace_fn)(void *file_handle); void (*backtrace_fn)(void *file_handle);
} callbacks; } callbacks;
@ -352,6 +353,13 @@ static CLG_LogType *clg_ctx_type_register(CLogContext *ctx, const char *identifi
return ty; return ty;
} }
static void clg_ctx_error_action(CLogContext *ctx)
{
if (ctx->callbacks.error_fn != NULL) {
ctx->callbacks.error_fn(ctx->output_file);
}
}
static void clg_ctx_fatal_action(CLogContext *ctx) static void clg_ctx_fatal_action(CLogContext *ctx)
{ {
if (ctx->callbacks.fatal_fn != NULL) { if (ctx->callbacks.fatal_fn != NULL) {
@ -522,6 +530,10 @@ void CLG_logf(CLG_LogType *lg,
clg_ctx_backtrace(lg->ctx); clg_ctx_backtrace(lg->ctx);
} }
if (severity == CLG_SEVERITY_ERROR) {
clg_ctx_error_action(lg->ctx);
}
if (severity == CLG_SEVERITY_FATAL) { if (severity == CLG_SEVERITY_FATAL) {
clg_ctx_fatal_action(lg->ctx); clg_ctx_fatal_action(lg->ctx);
} }
@ -555,6 +567,12 @@ static void CLG_ctx_output_use_timestamp_set(CLogContext *ctx, int value)
} }
} }
/** Action on error severity. */
static void CLT_ctx_error_fn_set(CLogContext *ctx, void (*error_fn)(void *file_handle))
{
ctx->callbacks.error_fn = error_fn;
}
/** Action on fatal severity. */ /** Action on fatal severity. */
static void CLG_ctx_fatal_fn_set(CLogContext *ctx, void (*fatal_fn)(void *file_handle)) static void CLG_ctx_fatal_fn_set(CLogContext *ctx, void (*fatal_fn)(void *file_handle))
{ {
@ -674,6 +692,11 @@ void CLG_output_use_timestamp_set(int value)
CLG_ctx_output_use_timestamp_set(g_ctx, value); CLG_ctx_output_use_timestamp_set(g_ctx, value);
} }
void CLG_error_fn_set(void (*error_fn)(void *file_handle))
{
CLT_ctx_error_fn_set(g_ctx, error_fn);
}
void CLG_fatal_fn_set(void (*fatal_fn)(void *file_handle)) void CLG_fatal_fn_set(void (*fatal_fn)(void *file_handle))
{ {
CLG_ctx_fatal_fn_set(g_ctx, fatal_fn); CLG_ctx_fatal_fn_set(g_ctx, fatal_fn);

@ -215,6 +215,11 @@ extern const char *(*MEM_name_ptr)(void *vmemh);
* about memory leaks will be printed on exit. */ * about memory leaks will be printed on exit. */
void MEM_init_memleak_detection(void); void MEM_init_memleak_detection(void);
/** When this has been called and memory leaks have been detected, the process will have an exit
* code that indicates failure. This can be used for when checking for memory leaks with automated
* tests. */
void MEM_enable_fail_on_memleak(void);
/* Switch allocator to slower but fully guarded mode. */ /* Switch allocator to slower but fully guarded mode. */
void MEM_use_guarded_allocator(void); void MEM_use_guarded_allocator(void);

@ -18,6 +18,8 @@
* \ingroup MEM * \ingroup MEM
*/ */
#include <cstdlib>
#include "MEM_guardedalloc.h" #include "MEM_guardedalloc.h"
#include "mallocn_intern.h" #include "mallocn_intern.h"
@ -28,6 +30,9 @@ char free_after_leak_detection_message[] =
"error, use the 'construct on first use' idiom."; "error, use the 'construct on first use' idiom.";
namespace { namespace {
static bool fail_on_memleak = false;
class MemLeakPrinter { class MemLeakPrinter {
public: public:
~MemLeakPrinter() ~MemLeakPrinter()
@ -42,6 +47,15 @@ class MemLeakPrinter {
leaked_blocks, leaked_blocks,
(double)mem_in_use / 1024 / 1024); (double)mem_in_use / 1024 / 1024);
MEM_printmemlist(); MEM_printmemlist();
if (fail_on_memleak) {
/* There are many other ways to change the exit code to failure here:
* - Make the destructor noexcept(false) and throw an exception.
* - Call exit(EXIT_FAILURE).
* - Call terminate().
*/
abort();
}
} }
}; };
} // namespace } // namespace
@ -59,3 +73,8 @@ void MEM_init_memleak_detection(void)
*/ */
static MemLeakPrinter printer; static MemLeakPrinter printer;
} }
void MEM_enable_fail_on_memleak(void)
{
fail_on_memleak = true;
}

@ -751,6 +751,25 @@ static int arg_handle_abort_handler_disable(int UNUSED(argc),
return 0; return 0;
} }
static void clog_abort_on_error_callback(void *fp)
{
BLI_system_backtrace(fp);
fflush(fp);
abort();
}
static const char arg_handle_debug_exit_on_error_doc[] =
"\n\t"
"Immediately exit when internal errors are detected.";
static int arg_handle_debug_exit_on_error(int UNUSED(argc),
const char **UNUSED(argv),
void *UNUSED(data))
{
MEM_enable_fail_on_memleak();
CLG_error_fn_set(clog_abort_on_error_callback);
return 0;
}
static const char arg_handle_background_mode_set_doc[] = static const char arg_handle_background_mode_set_doc[] =
"\n\t" "\n\t"
"Run in background (often used for UI-less rendering)."; "Run in background (often used for UI-less rendering).";
@ -2214,6 +2233,7 @@ void main_args_setup(bContext *C, bArgs *ba)
"--debug-gpu-force-workarounds", "--debug-gpu-force-workarounds",
CB_EX(arg_handle_debug_mode_generic_set, gpumem), CB_EX(arg_handle_debug_mode_generic_set, gpumem),
(void *)G_DEBUG_GPU_FORCE_WORKAROUNDS); (void *)G_DEBUG_GPU_FORCE_WORKAROUNDS);
BLI_argsAdd(ba, 1, NULL, "--debug-exit-on-error", CB(arg_handle_debug_exit_on_error), NULL);
BLI_argsAdd(ba, 1, NULL, "--verbose", CB(arg_handle_verbosity_set), NULL); BLI_argsAdd(ba, 1, NULL, "--verbose", CB(arg_handle_verbosity_set), NULL);

@ -44,7 +44,7 @@ unset(_default_test_python_exe)
# Standard Blender arguments for running tests. # Standard Blender arguments for running tests.
# Specify exit code so that if a Python script error happens, the test fails. # Specify exit code so that if a Python script error happens, the test fails.
set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --python-exit-code 1) set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --debug-memory --debug-exit-on-error --python-exit-code 1)
# Python CTests # Python CTests
if(WITH_BLENDER AND WITH_PYTHON) if(WITH_BLENDER AND WITH_PYTHON)

@ -50,6 +50,7 @@ int main(int argc, char **argv)
{ {
MEM_use_guarded_allocator(); MEM_use_guarded_allocator();
MEM_init_memleak_detection(); MEM_init_memleak_detection();
MEM_enable_fail_on_memleak();
testing::InitGoogleTest(&argc, argv); testing::InitGoogleTest(&argc, argv);
BLENDER_GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true); BLENDER_GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
google::InitGoogleLogging(argv[0]); google::InitGoogleLogging(argv[0]);

@ -36,12 +36,12 @@ execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${TEST_OUT_DIR})
# all calls to blender use this # all calls to blender use this
if(APPLE) if(APPLE)
if(${CMAKE_GENERATOR} MATCHES "Xcode") if(${CMAKE_GENERATOR} MATCHES "Xcode")
set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup) set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --debug-memory --debug-exit-on-error)
else() else()
set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --env-system-scripts ${CMAKE_SOURCE_DIR}/release/scripts) set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --debug-memory --debug-exit-on-error --env-system-scripts ${CMAKE_SOURCE_DIR}/release/scripts)
endif() endif()
else() else()
set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --env-system-scripts ${CMAKE_SOURCE_DIR}/release/scripts) set(TEST_BLENDER_EXE_PARAMS --background -noaudio --factory-startup --debug-memory --debug-exit-on-error --env-system-scripts ${CMAKE_SOURCE_DIR}/release/scripts)
endif() endif()
# for testing with valgrind prefix: valgrind --track-origins=yes --error-limit=no # for testing with valgrind prefix: valgrind --track-origins=yes --error-limit=no

@ -20,6 +20,8 @@ def get_arguments(filepath, output_filepath):
"-noaudio", "-noaudio",
"--factory-startup", "--factory-startup",
"--enable-autoexec", "--enable-autoexec",
"--debug-memory",
"--debug-exit-on-error",
filepath, filepath,
"-E", "CYCLES", "-E", "CYCLES",
"-o", output_filepath, "-o", output_filepath,

@ -103,6 +103,8 @@ def get_arguments(filepath, output_filepath):
"-noaudio", "-noaudio",
"--factory-startup", "--factory-startup",
"--enable-autoexec", "--enable-autoexec",
"--debug-memory",
"--debug-exit-on-error",
filepath, filepath,
"-E", "BLENDER_EEVEE", "-E", "BLENDER_EEVEE",
"-P", "-P",

@ -448,16 +448,17 @@ class Report:
crash = False crash = False
output = None output = None
try: try:
output = subprocess.check_output(command) completed_process = subprocess.run(command, stdout=subprocess.PIPE)
except subprocess.CalledProcessError as e: if completed_process.returncode != 0:
crash = True crash = True
output = completed_process.stdout
except BaseException as e: except BaseException as e:
crash = True crash = True
if verbose: if verbose:
print(" ".join(command)) print(" ".join(command))
if output: if (verbose or crash) and output:
print(output.decode("utf-8")) print(output.decode("utf-8"))
# Detect missing filepaths and consider those errors # Detect missing filepaths and consider those errors
for filepath, output_filepath in zip(remaining_filepaths[:], output_filepaths): for filepath, output_filepath in zip(remaining_filepaths[:], output_filepaths):

@ -79,6 +79,8 @@ class AbstractBlenderRunnerTest(unittest.TestCase):
'-noaudio', '-noaudio',
'--factory-startup', '--factory-startup',
'--enable-autoexec', '--enable-autoexec',
'--debug-memory',
'--debug-exit-on-error',
] ]
if blendfile: if blendfile:

@ -39,6 +39,8 @@ def get_arguments(filepath, output_filepath):
"-noaudio", "-noaudio",
"--factory-startup", "--factory-startup",
"--enable-autoexec", "--enable-autoexec",
"--debug-memory",
"--debug-exit-on-error",
filepath, filepath,
"-P", "-P",
os.path.realpath(__file__), os.path.realpath(__file__),

@ -30,7 +30,7 @@ execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${TEST_OUT_DIR})
# endif() # endif()
# for testing with valgrind prefix: valgrind --track-origins=yes --error-limit=no # for testing with valgrind prefix: valgrind --track-origins=yes --error-limit=no
set(TEST_BLENDER_EXE $<TARGET_FILE:blender> --background -noaudio --factory-startup --env-system-scripts ${CMAKE_SOURCE_DIR}/release/scripts) set(TEST_BLENDER_EXE $<TARGET_FILE:blender> --background -noaudio --factory-startup --debug-memory --debug-exit-on-error --env-system-scripts ${CMAKE_SOURCE_DIR}/release/scripts)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

@ -39,6 +39,8 @@ def get_arguments(filepath, output_filepath):
"-noaudio", "-noaudio",
"--factory-startup", "--factory-startup",
"--enable-autoexec", "--enable-autoexec",
"--debug-memory",
"--debug-exit-on-error",
filepath, filepath,
"-E", "BLENDER_WORKBENCH", "-E", "BLENDER_WORKBENCH",
"-P", "-P",