From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTP id AD3EC273F2 for ; Fri, 23 Aug 2019 05:59:39 -0400 (EDT) Received: from turing.freelists.org ([127.0.0.1]) by localhost (turing.freelists.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id IPk4g_HIU-Qb for ; Fri, 23 Aug 2019 05:59:39 -0400 (EDT) Received: from smtpng3.m.smailru.net (smtpng3.m.smailru.net [94.100.177.149]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTPS id 09E54273D3 for ; Fri, 23 Aug 2019 05:59:38 -0400 (EDT) From: Kirill Shcherbatov Subject: [tarantool-patches] [PATCH v2 3/3] box: introduce stacked diagnostic area Date: Fri, 23 Aug 2019 12:59:30 +0300 Message-Id: In-Reply-To: References: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Sender: tarantool-patches-bounce@freelists.org Errors-to: tarantool-patches-bounce@freelists.org Reply-To: tarantool-patches@freelists.org List-Help: List-Unsubscribe: List-software: Ecartis version 1.0.0 List-Id: tarantool-patches List-Subscribe: List-Owner: List-post: List-Archive: To: tarantool-patches@freelists.org, georgy@tarantool.org Cc: alexander.turenko@tarantool.org, kostja@tarantool.org, Kirill Shcherbatov Closes #1148 @TarantoolBot document Title: Stacked Diagnostics for Tarantool Tarantool statements must produce diagnostic information that populates the diagnostics area. Diagnostics area stack must contain a diagnostics area for each nested execution context. Tarantool used to have the only diag_set() mechanism to set a diagnostic error. In some cases a diagnostic information must be more complicated. A new method diag_add() allows to extend global diagnostics with a new error: the previous set becomes a reason for a recently-constructed error object. This commit also introduce a savepoint-semantics mechanism to remove errors when they are redundant. Thus, diag_set(diag, FIRST) struct error *SVP = diag_svp(diag) diag_add(diag, SECOND) diag_add(diag, THIRD) diag_rollback_to_svp(diag, svp) // only the FIRST error is set To work with errors having a reason efficiently, box.error endpoint was extended with a new method prev that returns a reason error object for a given error, when exists or nil otherwise: err = box.error.last() err:unpack() reason = box.error.prev(err) reason:unpack() --- src/lib/core/diag.h | 78 +++++++++++++++- src/lua/error.h | 3 + src/box/key_list.c | 16 ++-- src/box/lua/call.c | 6 +- src/lib/core/diag.c | 1 + src/lua/error.c | 2 +- src/box/lua/error.cc | 20 ++++ src/lua/error.lua | 2 + test/engine/func_index.result | 48 ++++++++-- test/engine/func_index.test.lua | 7 ++ test/unit/CMakeLists.txt | 2 +- test/unit/fiber.cc | 157 ++++++++++++++++++++++++++++++++ test/unit/fiber.result | 60 ++++++++++++ 13 files changed, 379 insertions(+), 23 deletions(-) diff --git a/src/lib/core/diag.h b/src/lib/core/diag.h index 02a67269f..6d50e94e7 100644 --- a/src/lib/core/diag.h +++ b/src/lib/core/diag.h @@ -78,6 +78,8 @@ struct error { char file[DIAG_FILENAME_MAX]; /* Error description. */ char errmsg[DIAG_ERRMSG_MAX]; + /* A pointer to the reason error. */ + struct error *reason; }; static inline void @@ -90,9 +92,11 @@ static inline void error_unref(struct error *e) { assert(e->refs > 0); - --e->refs; - if (e->refs == 0) + if (--e->refs == 0) { + if (e->reason != NULL) + error_unref(e->reason); e->destroy(e); + } } NORETURN static inline void @@ -175,6 +179,26 @@ diag_set_error(struct diag *diag, struct error *e) diag->last = e; } +/** + * Add a new error to the diagnostics area: the previous error + * becomes a reason of a current. + * \param diag diagnostics area + * \param e error to add + */ +static inline void +diag_add_error(struct diag *diag, struct error *e) +{ + assert(e != NULL); + error_ref(e); + /* + * Nominally e takes a reason's reference while diag + * releases it's reference because it holds e now + * instead. I.e. reason->refs kept unchanged. + */ + e->reason = diag->last; + diag->last = e; +} + /** * Move all errors from \a from to \a to. * \param from source @@ -212,6 +236,45 @@ diag_last_error(struct diag *diag) return diag->last; } +/** + * Get a diagnostic savepoint: a marker that allows to reset all + * errors set after that moment. + */ +static inline struct error * +diag_svp(struct diag *diag) +{ + return diag_last_error(diag); +} + +/** + * Remove all errors set in a given diagnostics area after a + * given savepoint. + * + * Operation removes reason for the error + * preceding the savepoint and releases a diagnostic area's + * reference on the most recent error (diag::last for the + * rollback beginning). This means that if user code have a + * pointer and have a reference to an error object from the + * rollback zone, this pointer and the following "reason" error + * objects are a valid error list. + */ +static inline void +diag_rollback_to_svp(struct diag *diag, struct error *svp) +{ + struct error *begin = diag->last, *prev = NULL; + while (diag->last != svp) { + prev = diag->last; + diag->last = diag->last->reason; + } + if (diag->last != begin) { + assert(prev != NULL && prev->reason == svp); + prev->reason = NULL; + error_unref(begin); + prev->reason = svp; + error_ref(svp); + } +} + struct diag * diag_get(); @@ -274,6 +337,17 @@ BuildSocketError(const char *file, unsigned line, const char *socketname, errno = save_errno; \ } while (0) +#define diag_add(class, ...) do { \ + /* Preserve the original errno. */ \ + int save_errno = errno; \ + say_debug("%s at %s:%i", #class, __FILE__, __LINE__); \ + struct error *e; \ + e = Build##class(__FILE__, __LINE__, ##__VA_ARGS__); \ + diag_add_error(diag_get(), e); \ + /* Restore the errno which might have been reset. */ \ + errno = save_errno; \ +} while (0) + #if defined(__cplusplus) } /* extern "C" */ #endif /* defined(__cplusplus) */ diff --git a/src/lua/error.h b/src/lua/error.h index 64fa5eba3..16cdaf7fe 100644 --- a/src/lua/error.h +++ b/src/lua/error.h @@ -65,6 +65,9 @@ luaT_pusherror(struct lua_State *L, struct error *e); struct error * luaL_iserror(struct lua_State *L, int narg); +struct error * +luaL_checkerror(struct lua_State *L, int narg); + void tarantool_lua_error_init(struct lua_State *L); diff --git a/src/box/key_list.c b/src/box/key_list.c index e130d1c8c..c3de262cc 100644 --- a/src/box/key_list.c +++ b/src/box/key_list.c @@ -63,9 +63,9 @@ key_list_iterator_create(struct key_list_iterator *it, struct tuple *tuple, if (rc != 0) { /* Can't evaluate function. */ struct space *space = space_by_id(index_def->space_id); - diag_set(ClientError, ER_FUNC_INDEX_FUNC, index_def->name, - space ? space_name(space) : "", - diag_last_error(diag_get())->errmsg); + diag_add(ClientError, ER_FUNC_INDEX_FUNC, index_def->name, + space != NULL ? space_name(space) : "", + "can't evaluate function"); return -1; } uint32_t key_data_sz; @@ -74,9 +74,9 @@ key_list_iterator_create(struct key_list_iterator *it, struct tuple *tuple, if (key_data == NULL) { struct space *space = space_by_id(index_def->space_id); /* Can't get a result returned by function . */ - diag_set(ClientError, ER_FUNC_INDEX_FUNC, index_def->name, - space ? space_name(space) : "", - diag_last_error(diag_get())->errmsg); + diag_add(ClientError, ER_FUNC_INDEX_FUNC, index_def->name, + space != NULL ? space_name(space) : "", + "can't get a value returned by function"); return -1; } @@ -170,9 +170,9 @@ key_list_iterator_next(struct key_list_iterator *it, const char **value) * The key doesn't follow functional index key * definition. */ - diag_set(ClientError, ER_FUNC_INDEX_FORMAT, it->index_def->name, + diag_add(ClientError, ER_FUNC_INDEX_FORMAT, it->index_def->name, space ? space_name(space) : "", - diag_last_error(diag_get())->errmsg); + "key does not follow functional index definition"); return -1; } diff --git a/src/box/lua/call.c b/src/box/lua/call.c index 0ac2eb7a6..cebd8a680 100644 --- a/src/box/lua/call.c +++ b/src/box/lua/call.c @@ -680,9 +680,9 @@ func_persistent_lua_load(struct func_lua *func) if (func->base.def->is_sandboxed) { if (prepare_lua_sandbox(tarantool_L, default_sandbox_exports, nelem(default_sandbox_exports)) != 0) { - diag_set(ClientError, ER_LOAD_FUNCTION, - func->base.def->name, - diag_last_error(diag_get())->errmsg); + diag_add(ClientError, ER_LOAD_FUNCTION, + func->base.def->name, + "can't prepare a Lua sandbox"); goto end; } } else { diff --git a/src/lib/core/diag.c b/src/lib/core/diag.c index 248277e74..c0aeb52b9 100644 --- a/src/lib/core/diag.c +++ b/src/lib/core/diag.c @@ -52,6 +52,7 @@ error_create(struct error *e, e->line = 0; } e->errmsg[0] = '\0'; + e->reason = NULL; } struct diag * diff --git a/src/lua/error.c b/src/lua/error.c index d82e78dc4..18a990a88 100644 --- a/src/lua/error.c +++ b/src/lua/error.c @@ -53,7 +53,7 @@ luaL_iserror(struct lua_State *L, int narg) return e; } -static struct error * +struct error * luaL_checkerror(struct lua_State *L, int narg) { struct error *error = luaL_iserror(L, narg); diff --git a/src/box/lua/error.cc b/src/box/lua/error.cc index 230d51dec..da0a3f52c 100644 --- a/src/box/lua/error.cc +++ b/src/box/lua/error.cc @@ -144,6 +144,22 @@ luaT_error_new(lua_State *L) return luaT_error_last(L); } +static int +luaT_error_prev(lua_State *L) +{ + if (lua_gettop(L) == 0) + return luaL_error(L, "Usage: box.error.prev(error)"); + struct error *e = luaL_checkerror(L, 1); + if (e == NULL) + return luaT_error(L); + + if (e->reason != NULL) + luaT_pusherror(L, e->reason); + else + lua_pushnil(L); + return 1; +} + static int luaT_error_clear(lua_State *L) { @@ -250,6 +266,10 @@ box_lua_error_init(struct lua_State *L) { lua_pushcfunction(L, luaT_error_new); lua_setfield(L, -2, "new"); } + { + lua_pushcfunction(L, luaT_error_prev); + lua_setfield(L, -2, "prev"); + } lua_setfield(L, -2, "__index"); } lua_setmetatable(L, -2); diff --git a/src/lua/error.lua b/src/lua/error.lua index 28fc0377d..7a4d19c5a 100644 --- a/src/lua/error.lua +++ b/src/lua/error.lua @@ -23,6 +23,8 @@ struct error { char _file[DIAG_FILENAME_MAX]; /* Error description. */ char _errmsg[DIAG_ERRMSG_MAX]; + /* A pointer to the reason error. */ + struct error *_reason; }; char * diff --git a/test/engine/func_index.result b/test/engine/func_index.result index bb4200f7a..5ca7fcb66 100644 --- a/test/engine/func_index.result +++ b/test/engine/func_index.result @@ -5,6 +5,10 @@ test_run = require('test_run').new() engine = test_run:get_cfg('engine') | --- | ... +test_run:cmd("push filter \"file: .*\" to \"file: \"") + | --- + | - true + | ... -- -- gh-1260: Func index. @@ -158,8 +162,7 @@ idx = s:create_index('idx', {func = box.func.invalidreturn1.id, parts = {{1, 'un s:insert({1}) | --- | - error: 'Key format doesn''t match one defined in functional index ''idx'' of space - | ''withdata'': Supplied key type of part 0 does not match index part type: expected - | unsigned' + | ''withdata'': key does not follow functional index definition' | ... idx:drop() | --- @@ -197,8 +200,7 @@ idx = s:create_index('idx', {func = box.func.invalidreturn3.id, parts = {{1, 'un s:insert({1}) | --- | - error: 'Key format doesn''t match one defined in functional index ''idx'' of space - | ''withdata'': Supplied key type of part 0 does not match index part type: expected - | unsigned' + | ''withdata'': key does not follow functional index definition' | ... idx:drop() | --- @@ -217,8 +219,7 @@ idx = s:create_index('idx', {func = box.func.invalidreturn4.id, parts = {{1, 'un s:insert({1}) | --- | - error: 'Key format doesn''t match one defined in functional index ''idx'' of space - | ''withdata'': Supplied key type of part 0 does not match index part type: expected - | unsigned' + | ''withdata'': key does not follow functional index definition' | ... idx:drop() | --- @@ -264,8 +265,39 @@ idx = s:create_index('idx', {func = box.func.runtimeerror.id, parts = {{1, 'stri s:insert({1}) | --- | - error: 'Failed to build a key for functional index ''idx'' of space ''withdata'': - | [string "return function(tuple) local ..."]:1: attempt to call - | global ''require'' (a nil value)' + | can''t evaluate function' + | ... +e = box.error.last() + | --- + | ... +e:unpack() + | --- + | - type: ClientError + | code: 198 + | message: 'Failed to build a key for functional index ''idx'' of space ''withdata'': + | can''t evaluate function' + | trace: + | - file: + | line: 68 + | ... +e = box.error.prev(e) + | --- + | ... +e:unpack() + | --- + | - type: LuajitError + | message: '[string "return function(tuple) local ..."]:1: attempt + | to call global ''require'' (a nil value)' + | trace: + | - file: + | line: 1010 + | ... +e = box.error.prev(e) + | --- + | ... +e == nil + | --- + | - true | ... idx:drop() | --- diff --git a/test/engine/func_index.test.lua b/test/engine/func_index.test.lua index f31162c97..ccbc9822d 100644 --- a/test/engine/func_index.test.lua +++ b/test/engine/func_index.test.lua @@ -1,5 +1,6 @@ test_run = require('test_run').new() engine = test_run:get_cfg('engine') +test_run:cmd("push filter \"file: .*\" to \"file: \"") -- -- gh-1260: Func index. @@ -99,6 +100,12 @@ test_run:cmd("setopt delimiter ''"); box.schema.func.create('runtimeerror', {body = lua_code, is_deterministic = true, is_sandboxed = true}) idx = s:create_index('idx', {func = box.func.runtimeerror.id, parts = {{1, 'string'}}}) s:insert({1}) +e = box.error.last() +e:unpack() +e = box.error.prev(e) +e:unpack() +e = box.error.prev(e) +e == nil idx:drop() -- Remove old persistent functions diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 4a57597e9..6dfe1f6d9 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -72,7 +72,7 @@ target_link_libraries(decimal.test core unit) add_executable(fiber.test fiber.cc) set_source_files_properties(fiber.cc PROPERTIES COMPILE_FLAGS -O0) -target_link_libraries(fiber.test core unit) +target_link_libraries(fiber.test core box unit) if (NOT ENABLE_GCOV) # This test is known to be broken with GCOV diff --git a/test/unit/fiber.cc b/test/unit/fiber.cc index 91f7d43f9..9ff1cc643 100644 --- a/test/unit/fiber.cc +++ b/test/unit/fiber.cc @@ -1,3 +1,6 @@ +#include "box/error.h" +#include "diag.h" +#include "errcode.h" #include "memory.h" #include "fiber.h" #include "unit.h" @@ -181,12 +184,166 @@ fiber_name_test() footer(); } +void +diag_test() +{ + header(); + plan(28); + + note("constract e1 in global diag, share with local diag"); + diag_set(ClientError, ER_PROC_LUA, "runtime error"); + struct diag local_diag; + diag_create(&local_diag); + /* Copy a last error to the local diagnostics area. */ + diag_add_error(&local_diag, diag_last_error(diag_get())); + + struct error *e1 = diag_last_error(&local_diag); + is(e1, diag_last_error(diag_get()), + "e1 is an error shared between local and global diag"); + is(e1->refs, 2, "e1::refs: global+local diag"); + + note("append e2 to global diag, usr error_ref(e2)"); + diag_add(ClientError, ER_LOAD_FUNCTION, "test", "internal error"); + struct error *e2 = diag_last_error(diag_get()); + error_ref(e2); + + is(e2->reason, e1, "e2::reason == e1"); + is(e1->reason, NULL, "e1::reason == NULL"); + is(e1->refs, 2, "e1::refs: e2 + global diag"); + is(e2->refs, 2, "e2::refs: usr + global diag"); + + note("diag_clean global diag"); + diag_clear(diag_get()); + is(e1->refs, 2, "e1::refs: e2 + local"); + is(e2->refs, 1, "e2::refs: usr"); + note("error_unref(e2) -> e2 destruction"); + error_unref(e2); + e2 = NULL; + is(e1->refs, 1, "e1::refs: local diag"); + + /* Test rollback to SVP. */ + note("diag_move(from = local, to = global): move e1 to global"); + diag_move(&local_diag, diag_get()); + is(diag_is_empty(&local_diag), true, "local diag is empty"); + is(diag_is_empty(diag_get()), false, "global diag is not empty"); + is(diag_last_error(diag_get()), e1, "global diag::last == e1"); + + note("svp = diag_svp(global), i.e. 'diag_last(global) = e1' state"); + struct error *svp = diag_svp(diag_get()); + fail_if(svp != e1); + fail_if(diag_last_error(diag_get()) != e1); + fail_if(e1->reason != NULL); + note("append e3, e4 to global diag"); + note("usr error_ref(e1), error_ref(e3), error_ref(e4)"); + diag_add(ClientError, ER_LOAD_FUNCTION, "test", "internal error"); + struct error *e3 = diag_last_error(diag_get()); + error_ref(e3); + diag_add(ClientError, ER_FUNC_INDEX_FUNC, "func_idx", "space", + "everything is bad"); + struct error *e4 = diag_last_error(diag_get()); + error_ref(e4); + is(e1->refs, 1, "e1::refs: e3"); + is(e3->refs, 2, "e3::refs: usr + e4"); + is(e4->refs, 2, "e4::refs: usr + global diag"); + is(e4->reason, e3, "e4::reason == e3"); + is(e3->reason, e1, "e3::reason == e1"); + note("diag_rollback_to_svp(global, svp)"); + /* + * Before rollback there is a sequence + * DIAG[e4]->e3->e1->NULL; + * After rollback there would be DIAG[e1]->NULL and + * a sequence e4->e3->e1->NULL. + */ + diag_rollback_to_svp(diag_get(), svp); + is(e1->refs, 2, "e1::refs: e3 + global diag %d/%d", e1->refs, 2); + is(e3->refs, 2, "e3::refs: usr + e4"); + is(e4->refs, 1, "e4::refs: usr"); + is(diag_last_error(diag_get()), e1, "diag_last(global) = e1"); + /* Rollback doesn't change error objects itself. */ + is(e4->reason, e3, "e4::reason == e3"); + is(e3->reason, e1, "e3::reason == e1"); + error_unref(e4); + e4 = NULL; + is(e3->refs, 1, "e3::refs: usr"); + error_unref(e3); + e3 = NULL; + + note("ensure that sequential rollback is no-op"); + diag_rollback_to_svp(diag_get(), svp); + is(e1->refs, 1, "e1::refs: global diag"); + + diag_clear(diag_get()); + /* + * usr ref SVP + * DEL! | | + * DIAG[#5] -> #4 -> DIAG'[#3] -> #2 -> #1 + * + * 1) diag_rollback_to_svp + * del <-----------<------> + */ + note("test partial list destruction on rollback"); + diag_add(ClientError, ER_PROC_LUA, "#1"); + struct error *er1 = diag_last_error(diag_get()); + diag_add(ClientError, ER_PROC_LUA, "#2"); + svp = diag_svp(diag_get()); + diag_add(ClientError, ER_PROC_LUA, "#3"); + struct error *er3 = diag_last_error(diag_get()); + diag_add(ClientError, ER_PROC_LUA, "#4"); + struct error *er4 = diag_last_error(diag_get()); + error_ref(er4); + diag_add(ClientError, ER_PROC_LUA, "#5"); + is(er4->refs, 2, "er4:refs: usr + er5 %d/%d", er4->refs, 2); + + diag_rollback_to_svp(diag_get(), svp); + note("rollback to svp(er2) -> e5:refs == 0, destruction"); + is(er4->reason, er3, "er4->reason == er3"); + is(er3->refs, 1, "er3:refs: er4"); + is(er3->reason, svp, "er3->reason == svp"); + is(svp->refs, 2, "svp->refs: global diag + er3"); + is(svp->reason, er1, "svp->reason == er1"); + is(er1->refs, 1, "er1->refs: err2"); + + /* + * usr ref SVP + * | | + * #4 -> #3 -> DIAG'[#2] -> #1 + * | + * DIAG[#7] -> #6 -/ + * | + * usr ref + */ + note("multiple error sequences after rollback"); + diag_add(ClientError, ER_PROC_LUA, "#6"); + diag_add(ClientError, ER_PROC_LUA, "#7"); + struct error *er7 = diag_last_error(diag_get()); + error_ref(er7); + is(er4->refs, 1, "er4->refs: usr"); + is(er7->refs, 2, "er7->refs: global diag + usr"); + is(svp->refs, 2, "svp->refs: er3 + er6"); + is(svp->reason->refs, 1, "svp->reason->refs: svp"); + diag_rollback_to_svp(diag_get(), svp); + is(er4->refs, 1, "er4->refs: usr"); + is(er7->refs, 1, "er7->refs: usr"); + is(svp->refs, 3, "svp->refs: global diag + er3 + er6"); + is(svp->reason->refs, 1, "svp->reason->refs: svp"); + diag_clear(diag_get()); + is(svp->refs, 2, "svp->refs: er3 + er6"); + is(svp->reason->refs, 1, "svp->reason->refs: svp"); + error_unref(er4); + is(svp->refs, 1, "svp->refs: er6"); + is(svp->reason->refs, 1, "svp->reason->refs: svp"); + error_unref(er7); + + footer(); +} + static int main_f(va_list ap) { fiber_name_test(); fiber_join_test(); fiber_stack_test(); + diag_test(); ev_break(loop(), EVBREAK_ALL); return 0; } diff --git a/test/unit/fiber.result b/test/unit/fiber.result index 7c9f85dcd..0047426d1 100644 --- a/test/unit/fiber.result +++ b/test/unit/fiber.result @@ -17,3 +17,63 @@ SystemError Failed to allocate 42 bytes in allocator for exception: Cannot alloc # normal-stack fiber not crashed # big-stack fiber not crashed *** fiber_stack_test: done *** + *** diag_test *** +1..28 +# constract e1 in global diag, share with local diag +ok 1 - e1 is an error shared between local and global diag +ok 2 - e1::refs: global+local diag +# append e2 to global diag, usr error_ref(e2) +ok 3 - e2::reason == e1 +ok 4 - e1::reason == NULL +ok 5 - e1::refs: e2 + global diag +ok 6 - e2::refs: usr + global diag +# diag_clean global diag +ok 7 - e1::refs: e2 + local +ok 8 - e2::refs: usr +# error_unref(e2) -> e2 destruction +ok 9 - e1::refs: local diag +# diag_move(from = local, to = global): move e1 to global +ok 10 - local diag is empty +ok 11 - global diag is not empty +ok 12 - global diag::last == e1 +# svp = diag_svp(global), i.e. 'diag_last(global) = e1' state +# append e3, e4 to global diag +# usr error_ref(e1), error_ref(e3), error_ref(e4) +ok 13 - e1::refs: e3 +ok 14 - e3::refs: usr + e4 +ok 15 - e4::refs: usr + global diag +ok 16 - e4::reason == e3 +ok 17 - e3::reason == e1 +# diag_rollback_to_svp(global, svp) +ok 18 - e1::refs: e3 + global diag 2/2 +ok 19 - e3::refs: usr + e4 +ok 20 - e4::refs: usr +ok 21 - diag_last(global) = e1 +ok 22 - e4::reason == e3 +ok 23 - e3::reason == e1 +ok 24 - e3::refs: usr +# ensure that sequential rollback is no-op +ok 25 - e1::refs: global diag +# test partial list destruction on rollback +ok 26 - er4:refs: usr + er5 2/2 +# rollback to svp(er2) -> e5:refs == 0, destruction +ok 27 - er4->reason == er3 +ok 28 - er3:refs: er4 +ok 29 - er3->reason == svp +ok 30 - svp->refs: global diag + er3 +ok 31 - svp->reason == er1 +ok 32 - er1->refs: err2 +# multiple error sequences after rollback +ok 33 - er4->refs: usr +ok 34 - er7->refs: global diag + usr +ok 35 - svp->refs: er3 + er6 +ok 36 - svp->reason->refs: svp +ok 37 - er4->refs: usr +ok 38 - er7->refs: usr +ok 39 - svp->refs: global diag + er3 + er6 +ok 40 - svp->reason->refs: svp +ok 41 - svp->refs: er3 + er6 +ok 42 - svp->reason->refs: svp +ok 43 - svp->refs: er6 +ok 44 - svp->reason->refs: svp + *** diag_test: done *** -- 2.22.1