From 6a25675c3752a2804caeb4c5dce3caba16f765de Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 20 Mar 2020 17:09:01 +0100 Subject: [PATCH] Add support for deferring cleanup operations This patch adds two main features: - ability to offer resources to the test system - ability to queue cleanup operations Signed-off-by: Zygmunt Krynicki --- GNUmakefile | 20 ++++-- NEWS | 29 ++++++++ libzt.def | 2 + libzt.export_list | 2 + libzt.map | 2 + man/ZT_TRUE.3.in | 2 +- man/zt_check.3.in | 12 +++- man/zt_closure.3.in | 57 +++++++++++++++ man/zt_closure_func0.3.in | 24 +++++++ man/zt_closure_func1.3.in | 25 +++++++ man/zt_defer.3.in | 91 ++++++++++++++++++++++++ man/zt_pack_closure0.3.in | 63 +++++++++++++++++ man/zt_pack_closure1.3.in | 68 ++++++++++++++++++ man/zt_visit_resource.3.in | 0 zt-test.c | 141 +++++++++++++++++++++++++++++++++++++ zt.c | 138 +++++++++++++++++++++++++++++++++++- zt.h | 47 +++++++++++++ 17 files changed, 709 insertions(+), 14 deletions(-) create mode 100644 man/zt_closure.3.in create mode 100644 man/zt_closure_func0.3.in create mode 100644 man/zt_closure_func1.3.in create mode 100644 man/zt_defer.3.in create mode 100644 man/zt_pack_closure0.3.in create mode 100644 man/zt_pack_closure1.3.in create mode 100644 man/zt_visit_resource.3.in diff --git a/GNUmakefile b/GNUmakefile index 975fdc6..35c43b8 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -30,10 +30,6 @@ $(eval $(call ZMK.Import,GitVersion)) # Manual pages manpages = \ - libzt-test.1 \ - libzt.3 \ - zt_check.3 \ - zt_claim.3 \ ZT_CMP_BOOL.3 \ ZT_CMP_INT.3 \ ZT_CMP_PTR.3 \ @@ -41,12 +37,23 @@ manpages = \ ZT_CMP_UINT.3 \ ZT_CURRENT_LOCATION.3 \ ZT_FALSE.3 \ + ZT_NOT_NULL.3 \ + ZT_NULL.3 \ + ZT_TRUE.3 \ + libzt-test.1 \ + libzt.3 \ + zt_check.3 \ + zt_claim.3 \ + zt_closure.3 \ + zt_closure_func0.3 \ + zt_closure_func1.3 \ + zt_defer.3 \ zt_location.3 \ zt_location_at.3 \ zt_main.3 \ - ZT_NOT_NULL.3 \ - ZT_NULL.3 \ zt_pack_boolean.3 \ + zt_pack_closure0.3 \ + zt_pack_closure1.3 \ zt_pack_integer.3 \ zt_pack_nothing.3 \ zt_pack_pointer.3 \ @@ -56,7 +63,6 @@ manpages = \ zt_test.3 \ zt_test_case_func.3 \ zt_test_suite_func.3 \ - ZT_TRUE.3 \ zt_value.3 \ zt_visit_test_case.3 \ zt_visitor.3 diff --git a/NEWS b/NEWS index f342605..4bec850 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,20 @@ +Changes in 0.4 (unreleased): + + * Test suites can now offer resources, through the zt_visit_resource() + function. Resources are remembered by the library and used in specific + circumstances. This mode decouples allocation of resources such as memory + or threads from the internals of the library and puts ultimate control in + the hand of the test author. + + * Test functions can now defer cleanup operations through the zt_defer() + function and the family of supporting types, functions and macros. + Deferred functions execute when the test function returns, both + successfully and abnormally, through non-local exit. + + To use deferred cleanup, the test suite that either directly or indirectly + contains a test function must offer the resource ZT_RESOURCE_DEFER_CLOSURES, + which represent closures that can be prepared and called at a later moment. + Changes in 0.3.1: * The build system has been updated to ZMK 0.3.5. This should fix @@ -49,6 +66,18 @@ Changes in 0.3: tests cross builds libzt for DOS and tests it with DosBox and DOS/32 extender. + * Test suites can now provide resources available to test functions. + Resources are provided using the new function zt_visit_resource(). + The only resource currently available are defer slots. + + * Using zt_defer() tests can arrange for reliable execution of functions + even in the case of non-local test exit. Each use requires a defer slot + that must be provided as a resource by the test suite. If defer slots + are unavailable the function that was being registered is invoked + immediately and the test fails as if zt_assert() had failed. This ensures + defer mechanism is reliable and can be used to manage global state + or release system resources. + Changes in 0.2: * Argument type to all unit test functions was typedef'd diff --git a/libzt.def b/libzt.def index 7dd3a89..311b1d9 100644 --- a/libzt.def +++ b/libzt.def @@ -9,11 +9,13 @@ EXPORTS zt_cmp_ptr zt_cmp_rune zt_cmp_uint + zt_defer zt_false zt_main zt_not_null zt_null zt_pack_rune zt_true + zt_visit_resource zt_visit_test_case zt_visit_test_suite diff --git a/libzt.export_list b/libzt.export_list index 4e93738..2af79e1 100644 --- a/libzt.export_list +++ b/libzt.export_list @@ -6,11 +6,13 @@ _zt_cmp_int _zt_cmp_ptr _zt_cmp_rune _zt_cmp_uint +_zt_defer _zt_false _zt_main _zt_not_null _zt_null _zt_pack_rune _zt_true +_zt_visit_resource _zt_visit_test_case _zt_visit_test_suite diff --git a/libzt.map b/libzt.map index 406efbe..c29f25b 100644 --- a/libzt.map +++ b/libzt.map @@ -25,4 +25,6 @@ VERS_0_2 { VERS_0_3 { global: zt_cmp_ptr; + zt_visit_resource; + zt_defer; } VERS_0_2; diff --git a/man/ZT_TRUE.3.in b/man/ZT_TRUE.3.in index 5afa94e..e4de79b 100644 --- a/man/ZT_TRUE.3.in +++ b/man/ZT_TRUE.3.in @@ -11,7 +11,7 @@ #define ZT_TRUE(value) \\ zt_true( \\ ZT_CURRENT_LOCATION(), \\ - zt_pack_boolean((value), #value)) \\ + zt_pack_boolean((value), #value)) .Ed .Ft zt_claim .Fo zt_true diff --git a/man/zt_check.3.in b/man/zt_check.3.in index 04c3ead..7c53c13 100644 --- a/man/zt_check.3.in +++ b/man/zt_check.3.in @@ -57,10 +57,16 @@ test on failure. Non-local exit from .Fn zt_assert is implemented using -.Fn siglongjump . -One should not depend on neither C++ destructors nor the GCC cleanup function extension for correct resource management. +.Fn siglongjump +or +.Fn longjump , +depending on platform support. One should not depend on neither C++ +destructors nor the GCC cleanup function extension for correct resource +management. Instead, please use +.Fn zt_defer +to arrange a function to be called after the test function returns. .Sh RETURN VALUES -Neither function return anything. Failures are remembered inside the opaque +Neither function returns anything. Failures are remembered inside the opaque .Nm zt_test structure passed by pointer (aka .Nm zt_t ) diff --git a/man/zt_closure.3.in b/man/zt_closure.3.in new file mode 100644 index 0000000..42b81ba --- /dev/null +++ b/man/zt_closure.3.in @@ -0,0 +1,57 @@ +.Dd January 14, 2020 +.Os libzt @VERSION@ +.Dt zt_closure 3 PRM +.Sh NAME +.Nm zt_closure +.Nd structure representing a function closure +.Sh SYNOPSIS +.In zt.h +.Bd -literal +typedef struct zt_closure { + union { + zt_closure_func0 args0; + zt_closure_func1 args1; + } func; + zt_location location; + int nargs; + zt_value args[1]; +} zt_closure; +.Be +.Sh DESCRIPTION +The structure +.Nm +contains a function pointer and at most one argument represented as a +.Nm zt_value . +.Pp +Closures are used by +.Fn zt_defer +to facilitate reliable management of memory and other resources inside test +code. It can be also used to setup and restore mocking of application +behaviors. Function arguments are represented by +.Nm zt_value +for convenience, to avoid casting integers and pointers. Test code is +expected to provide wrapper functions that take a +.Nm zt_value +argument and pass the call to another function such as +.Fn free +or +.Fn close . +.Sh IMPLEMENTATION NOTES +The +.Nm location +field describes location in the source code where the closure was packed. It +is used in some diagnostic error messages. +.Sh SEE ALSO +.Xr zt_closure_func0 3 , +.Xr zt_closure_func1 3 , +.Xr zt_defer 3 , +.Xr zt_location 3 , +.Xr zt_pack_closure0 3 , +.Xr zt_pack_closure1 3 , +.Xr zt_value 3 +.Sh HISTORY +The structure +.Nm +first appeared in libzt 0.4 +.Sh AUTHORS +.An "Zygmunt Krynicki" Aq Mt me@zygoon.pl \ No newline at end of file diff --git a/man/zt_closure_func0.3.in b/man/zt_closure_func0.3.in new file mode 100644 index 0000000..07c3078 --- /dev/null +++ b/man/zt_closure_func0.3.in @@ -0,0 +1,24 @@ +.Dd January 14, 2020 +.Os libzt @VERSION@ +.Dt zt_closure_func0 3 PRM +.Sh NAME +.Nm zt_closure_func0 +.Nd function type for closures without arguments +.Sh SYNOPSIS +.In zt.h +.Ft typedef void +.Fo (*zt_closure_func0) +.Fa "void" +.Fc +.Sh DESCRIPTION +.Nm +is a function type that takes no arguments. It is used by +.Nm zt_closure . +.Sh SEE ALSO +.Xr zt_closure 3 +.Sh HISTORY +The type +.Nm +first appeared in libzt 0.4 +.Sh AUTHORS +.An "Zygmunt Krynicki" Aq Mt me@zygoon.pl diff --git a/man/zt_closure_func1.3.in b/man/zt_closure_func1.3.in new file mode 100644 index 0000000..a5ea8fd --- /dev/null +++ b/man/zt_closure_func1.3.in @@ -0,0 +1,25 @@ +.Dd January 14, 2020 +.Os libzt @VERSION@ +.Dt zt_closure_func1 3 PRM +.Sh NAME +.Nm zt_closure_func1 +.Nd function type for closures with one argument +.Sh SYNOPSIS +.In zt.h +.Ft typedef void +.Fo (*zt_closure_func1) +.Fa "zt_value arg1" +.Fc +.Sh DESCRIPTION +.Nm +is a function type that takes one argument. It is used by +.Nm zt_closure . +.Sh SEE ALSO +.Xr zt_closure 3 , +.Xr zt_value 3 +.Sh HISTORY +The type +.Nm +first appeared in libzt 0.4 +.Sh AUTHORS +.An "Zygmunt Krynicki" Aq Mt me@zygoon.pl diff --git a/man/zt_defer.3.in b/man/zt_defer.3.in new file mode 100644 index 0000000..51c8ea7 --- /dev/null +++ b/man/zt_defer.3.in @@ -0,0 +1,91 @@ +.Dd January 14, 2020 +.Os libzt @VERSION@ +.Dt zt_defer 3 PRM +.Sh NAME +.Nm zt_defer, +.Nd call a function after the current test terminates +.Sh SYNOPSIS +.In zt.h +.Ft void +.Fo zt_defer +.Fa "zt_t t" +.Fa "zt_closure closure" +.Fc +.Sh DESCRIPTION +The function +.Fn +arranges for the function encapsulated by +.Em closure +to be called at after the current test function returns. +This mechanism works even in the case of non-local exit +used by +.Fn zt_assert . +It is designed to allow test authors to reclaim allocated memory, close +files, restore the state of mocked state or other tasks that are required +to ensure program consistency. +.Pp +In the spirit of robustness, the burden of providing memory for deferred +function calls is shifted to the test author. Test authors must provide a +.Em test resource +of the kind +.Em ZT_RESOURCE_DEFER_CLOSURES +with enough capacity to remember all of the deferred calls in all the tests. +This can be done by calling +.Fn zt_visit_resource +anywhere in a test suite function, before visiting the first test that relies +on defer. +.Pp +If insufficient amount of defer resources are available, the defer function +is called immediately, the current test is marked as failed and the test +terminates through a non-local exit. Note that succesfully deferred calls +are also executed regardless of the means of exiting from a test function. +.Sh IMPLEMENTATION NOTES +Defer is implemented using a stack of +.Nm zt_closure +structures. Before visiting a test function, the stack is emptied. +At the end of the test subsequent closures are popped and executed. +.Sh RETURN VALUES +.Nm +does not return any value. +.Sh EXAMPLES +The following example shows a minimal test program using resources and defer +to ensure that allocated memory is not leaked. +.Bd -literal -offset indent +#include +#include + +static void free_value(zt_value val) { + free(val.as.pointer); +} + +#define FREE(ptr) ZT_CLOSURE1(free_value, zt_pack_pointer((ptr), #ptr)) + +static void test_allocated_memory(zt_t t) { + void *m = malloc(100); + zt_assert(t, ZT_NOT_NULL(m)); + zt_defer(t, FREE(m)); + /* Do something with the allocated memory. */ +} + +static void test_suite(zt_visitor v) { + zt_closure closures[1]; + zt_visit_resource(v, ZT_RESOURCE_DEFER_CLOSURES, + sizeof closures / sizeof *closures, closures); + ZT_VISIT_TEST_CASE(v, test_allocated_memory); +} + +int main(int argc, char** argv, char** envp) { + return zt_main(argc, argv, envp, test_suite); +} +.Ed +.Sh SEE ALSO +.Xr ZT_CLOSURE0 3 , +.Xr ZT_CLOSURE1 3 , +.Xr zt_closure 3 , +.Xr zt_visit_resource 3 +.Sh HISTORY +The function +.Nm +first appeared in libzt 0.4 +.Sh AUTHORS +.An "Zygmunt Krynicki" Aq Mt me@zygoon.pl diff --git a/man/zt_pack_closure0.3.in b/man/zt_pack_closure0.3.in new file mode 100644 index 0000000..950740f --- /dev/null +++ b/man/zt_pack_closure0.3.in @@ -0,0 +1,63 @@ +.Dd January 14, 2020 +.Os libzt @VERSION@ +.Dt zt_pack_closure0 3 PRM +.Sh NAME +.Nm zt_pack_closure0 , +.Nm ZT_CLOSURE0 +.Nd pack function closure without arguments +.Sh SYNOPSIS +.In zt.h +.Ft zt_closure +.Fo zt_pack_closure0 +.Fa "zt_location location" +.Fa "zt_closure_func0 func" +.Fc +.Bd -literal +#define ZT_CLOSURE0(func) \\ + zt_pack_closure0( \\ + ZT_CURRENT_LOCATION(), (func)) +.Be +.Sh DESCRIPTION +.Fn zt_pack_closure0 +packs a function pointer into +.Nm zt_closure +and sets additional information about the number and composition the +arguments, so that the function can be called at a later time. +.Pp +Actual test code should use +.Fn ZT_CLOSURE0 +which passes the first argument automatically. +.Sh IMPLEMENTATION NOTES +.Nm zt_pack_closure0 +is only provided as a static inline function. +.Sh RETURN VALUES +.Nm +returns the configured +.Nm zt_closure . +.Sh EXAMPLES +.Bd -literal -offset indent +#include + +void mock_global_stuff(void); +void restore_global_stuff(void); + +static void test_root_user(zt_t t) { + mock_global_stuff(); + zt_defer(t, ZT_CLOSURE0(restore_global_state)); + /* test something with the mocking in place. */ +} +.Ed +.Sh SEE ALSO +.Xr ZT_CURRENT_LOCATION 3 , +.Xr zt_closure 3 , +.Xr zt_closure_func0 3 , +.Xr zt_defer 3 , +.Xr zt_location 3 +.Sh HISTORY +The function +.Fn zt_pack_closure0 +and the macro +.Fn ZT_CLOSURE0 +first appeared in libzt 0.4 +.Sh AUTHORS +.An "Zygmunt Krynicki" Aq Mt me@zygoon.pl \ No newline at end of file diff --git a/man/zt_pack_closure1.3.in b/man/zt_pack_closure1.3.in new file mode 100644 index 0000000..9bb67f5 --- /dev/null +++ b/man/zt_pack_closure1.3.in @@ -0,0 +1,68 @@ +.Dd January 14, 2020 +.Os libzt @VERSION@ +.Dt zt_pack_closure1 3 PRM +.Sh NAME +.Nm zt_pack_closure1 , +.Nm ZT_CLOSURE1 +.Nd pack function closure with one argument +.Sh SYNOPSIS +.In zt.h +.Ft zt_closure +.Fo zt_pack_closure1 +.Fa "zt_location location" +.Fa "zt_closure_func1 func" +.Fa "zt_value arg1" +.Fc +.Bd -literal +#define ZT_CLOSURE1(func, arg1) \\ + zt_pack_closure0( \\ + ZT_CURRENT_LOCATION(), (func), (arg1)) +.Be +.Sh DESCRIPTION +.Fn zt_pack_closure1 +packs a function pointer and a single argument into +.Nm zt_closure +and sets additional information about the number and composition the +arguments, so that the function can be called at a later time. +.Pp +Actual test code should use +.Fn ZT_CLOSURE1 +which passes the first argument automatically. +.Sh IMPLEMENTATION NOTES +.Nm zt_pack_closure1 +is only provided as a static inline function. +.Sh RETURN VALUES +.Nm +returns the configured +.Nm zt_closure . +.Sh EXAMPLES +.Bd -literal -offset indent +#include +#include + +static void free_value(zt_value val) { + free(val.as.pointer); +} + +#define FREE(ptr) ZT_CLOSURE1(free_value, zt_pack_pointer((ptr), #ptr)) + +static void test_allocated_memory(zt_t t) { + void *m = malloc(100); + zt_defer(t, FREE(m)); + /* do something with m */ +} +.Ed +.Sh SEE ALSO +.Xr ZT_CURRENT_LOCATION 3 , +.Xr zt_closure 3 , +.Xr zt_closure_func1 3 , +.Xr zt_defer 3 , +.Xr zt_location 3 +.Sh HISTORY +The function +.Fn zt_pack_closure1 +and the macro +.Fn ZT_CLOSURE1 +first appeared in libzt 0.4 +.Sh AUTHORS +.An "Zygmunt Krynicki" Aq Mt me@zygoon.pl \ No newline at end of file diff --git a/man/zt_visit_resource.3.in b/man/zt_visit_resource.3.in new file mode 100644 index 0000000..e69de29 diff --git a/zt-test.c b/zt-test.c index db823db..bc8ede5 100644 --- a/zt-test.c +++ b/zt-test.c @@ -2249,6 +2249,143 @@ static void test_stdout_stderr(void) assert(zt_stderr() == stderr); } +static bool selftest_defer0_called; +static void selftest_defer0_func(void) +{ + selftest_defer0_called = true; +} + +static bool selftest_defer1_called; +static zt_value selftest_defer1_arg1; +static void selftest_defer1_func(zt_value arg1) +{ + selftest_defer1_called = true; + selftest_defer1_arg1 = arg1; +} + +static void selftest_defer0(zt_t t) +{ + zt_defer(t, ZT_CLOSURE0(selftest_defer0_func)); +} + +static void selftest_defer1(zt_t t) +{ + zt_value v = zt_pack_integer(42, "42"); + zt_defer(t, ZT_CLOSURE1(selftest_defer1_func, v)); +} + +static void selftest_defer_and_resource_suite(zt_visitor v) +{ + zt_closure closures[1]; + zt_visit_resource(v, ZT_RESOURCE_DEFER_CLOSURES, 1, closures); + ZT_VISIT_TEST_CASE(v, selftest_defer0); + ZT_VISIT_TEST_CASE(v, selftest_defer1); +} + +static void test_defer_normal(void) +{ + selftest_defer0_called = false; + char* test_argv[] = { "a.out" }; + int exit_code; + + zt_mock_stdout = selftest_temporary_file(); + zt_mock_stderr = selftest_temporary_file(); + + selftest_defer0_called = false; + selftest_defer1_called = false; + selftest_defer1_arg1 = zt_pack_nothing(); + exit_code = zt_main(1, test_argv, NULL, selftest_defer_and_resource_suite); + assert(exit_code == EXIT_SUCCESS); + selftest_stream_eq(zt_mock_stdout, ""); + selftest_stream_eq(zt_mock_stderr, ""); + fclose(zt_mock_stdout); + fclose(zt_mock_stderr); + zt_mock_stdout = NULL; + zt_mock_stderr = NULL; + assert(selftest_defer0_called == true); + assert(selftest_defer1_called == true); + assert(selftest_defer1_arg1.kind == ZT_INTMAX); + assert(selftest_defer1_arg1.as.intmax == 42); +} + +static int selftest_defer_order_shared_counter; +static int selftest_defer_order_a_counter; +static void selftest_defer_order_a_func(void) +{ + selftest_defer_order_a_counter = ++selftest_defer_order_shared_counter; +} + +static int selftest_defer_order_b_counter; +static void selftest_defer_order_b_func(void) +{ + selftest_defer_order_b_counter = ++selftest_defer_order_shared_counter; +} + +static void selftest_defer_order(zt_t t) +{ + zt_defer(t, ZT_CLOSURE0(selftest_defer_order_a_func)); + zt_defer(t, ZT_CLOSURE0(selftest_defer_order_b_func)); +} + +static void selftest_defer_order_suite(zt_visitor v) +{ + zt_closure closures[2]; + zt_visit_resource(v, ZT_RESOURCE_DEFER_CLOSURES, 2, closures); + ZT_VISIT_TEST_CASE(v, selftest_defer_order); +} + +static void test_defer_order(void) +{ + char* test_argv[] = { "a.out" }; + int exit_code; + + zt_mock_stdout = selftest_temporary_file(); + zt_mock_stderr = selftest_temporary_file(); + + selftest_defer_order_shared_counter = 0; + selftest_defer_order_a_counter = 0; + selftest_defer_order_b_counter = 0; + exit_code = zt_main(1, test_argv, NULL, selftest_defer_order_suite); + assert(exit_code == EXIT_SUCCESS); + selftest_stream_eq(zt_mock_stdout, ""); + selftest_stream_eq(zt_mock_stderr, ""); + fclose(zt_mock_stdout); + fclose(zt_mock_stderr); + zt_mock_stdout = NULL; + zt_mock_stderr = NULL; + assert(selftest_defer_order_a_counter == 2); + assert(selftest_defer_order_b_counter == 1); +} + +static void selftest_defer_no_resource_suite(zt_visitor v) +{ + ZT_VISIT_TEST_CASE(v, selftest_defer0); +} + +static void test_defer_abnormal(void) +{ + selftest_defer0_called = false; + char* test_argv[] = { "a.out" }; + int exit_code; + + zt_mock_stdout = selftest_temporary_file(); + zt_mock_stderr = selftest_temporary_file(); + + selftest_defer0_called = false; + exit_code = zt_main(1, test_argv, NULL, selftest_defer_no_resource_suite); + assert(exit_code == EXIT_SUCCESS); + selftest_stream_eq(zt_mock_stdout, ""); + selftest_stream_eq_at( + zt_mock_stderr, __FILE__, __LINE__, + "%s:%d: insufficient defer slots, provide more than 0\n", + __FILE__, __LINE__ - 113); + fclose(zt_mock_stdout); + fclose(zt_mock_stderr); + zt_mock_stdout = NULL; + zt_mock_stderr = NULL; + assert(selftest_defer0_called == true); +} + int main(ZT_UNUSED int argc, ZT_UNUSED char** argv, ZT_UNUSED char** envp) { (void)argc; @@ -2343,6 +2480,10 @@ int main(ZT_UNUSED int argc, ZT_UNUSED char** argv, ZT_UNUSED char** envp) test_stdout_stderr(); + test_defer_normal(); + test_defer_abnormal(); + test_defer_order(); + printf("libzt self-test successful\n"); return 0; } diff --git a/zt.c b/zt.c index 80eb185..6573a6a 100644 --- a/zt.c +++ b/zt.c @@ -247,6 +247,14 @@ typedef enum zt_outcome { ZT_FAILED } zt_outcome; +/** zt_resources describe all resources available to a test */ +typedef struct zt_resources { + FILE* debug_stream; + zt_closure* defer_closures; + size_t defer_cap; + size_t defer_len; +} zt_resources; + typedef struct zt_test { #if defined(_WIN32) || defined(__WATCOMC__) jmp_buf jump_buffer; @@ -257,11 +265,13 @@ typedef struct zt_test { FILE* stream; zt_location location; /** location of the last verified claim. */ zt_outcome outcome; + zt_resources* resources; } zt_test; typedef struct zt_visitor_vtab { void (*visit_case)(void*, zt_test_case_func, const char* name); void (*visit_suite)(void*, zt_test_suite_func, const char* name); + void (*visit_resource)(void*, zt_resource_kind, size_t, void*); } zt_visitor_vtab; typedef struct zt_test_lister { @@ -276,6 +286,7 @@ typedef struct zt_test_runner { int num_passed; int num_failed; bool verbose; + zt_resources resources; } zt_test_runner; /** zt_verify0_func is a type of verification function with no arguments. */ @@ -337,6 +348,11 @@ void zt_visit_test_case(zt_visitor v, zt_test_case_func func, v.vtab->visit_case(v.id, func, name); } +void zt_visit_resource(zt_visitor v, zt_resource_kind kind, size_t count, void* data) +{ + v.vtab->visit_resource(v.id, kind, count, data); +} + /* Lister visitor */ static zt_visitor zt_visitor_from_test_lister(zt_test_lister* lister); @@ -360,9 +376,28 @@ static void zt_test_lister__visit_case(void* id, ZT_UNUSED zt_test_case_func fun fprintf(lister->stream, "%*c %s\n", lister->nesting * 3, '-', name); } +static void zt_test_lister__visit_resource(void* id, zt_resource_kind kind, size_t count, ZT_UNUSED void* data) +{ + zt_test_lister* lister = (zt_test_lister*)id; + const char* res_name; + switch (kind) { + case ZT_RESOURCE_DEBUG_STREAM: + res_name = "debug stream"; + break; + case ZT_RESOURCE_DEFER_CLOSURES: + res_name = "defer closures"; + break; + default: + res_name = "???"; + break; + } + fprintf(lister->stream, "%*c %s x %zd\n", lister->nesting * 3, 'R', res_name, count); +} + static const zt_visitor_vtab zt_test_lister__visitor_vtab = { /* .visit_case = */ zt_test_lister__visit_case, /* .visit_suite = */ zt_test_lister__visit_suite, + /* .visit_resource = */ zt_test_lister__visit_resource, }; static zt_visitor zt_visitor_from_test_lister(zt_test_lister* lister) @@ -385,6 +420,62 @@ static void zt_list_tests_from(FILE* stream, zt_test_suite_func tsuite) /* Runner visitor */ +static void zt_nonlocal_exit(zt_test* test, int code) +{ +#ifndef _WIN32 + siglongjmp(test->jump_buffer, code); +#else + longjmp(test->jump_buffer, code); +#endif + /* TODO: in C++ mode throw an exception. */ +} + +static void zt_invoke_closure(zt_t t, zt_closure* closure) +{ + switch (closure->nargs) { + case 0: + closure->func.args0(); + break; + case 1: + closure->func.args1(closure->args[0]); + break; + default: + zt_logf(t->stream, closure->location, "unsupported number of arguments: %i", closure->nargs); + t->outcome = ZT_FAILED; + break; + } +} + +static void zt_null_test_error(void) +{ + fprintf(stderr, "usage error, do not pass NULL test pointers"); +} + +static void zt_null_resources_error(void) +{ + fprintf(stderr, "internal error, cannot defer, test resources unavailable"); +} + +void zt_defer(zt_t test, zt_closure closure) +{ + zt_resources* res; + if (test == NULL) { + zt_null_test_error(); + return; + } + res = test->resources; + if (res == NULL) { + zt_null_resources_error(); + return; + } + if (res->defer_len == res->defer_cap) { + zt_invoke_closure(test, &closure); + zt_logf(test->stream, closure.location, "insufficient defer slots, provide more than %zd", res->defer_cap); + zt_nonlocal_exit(test, 1); + } + res->defer_closures[res->defer_len++] = closure; +} + static zt_visitor zt_visitor_from_test_runner(zt_test_runner* runner); static void zt_runner_visitor__visit_suite(void* id, zt_test_suite_func func, @@ -400,15 +491,30 @@ static void zt_runner_visitor__visit_suite(void* id, zt_test_suite_func func, runner->nesting--; } -static void zt_runner_visitor__visit_case(void* id, zt_test_case_func func, +static void zt_provide_resources_to_test(zt_test_runner* runner, zt_test* test) +{ + test->stream = runner->stream_err; + test->resources = &runner->resources; + runner->resources.defer_len = 0; +} + +static void zt_run_defer_funcs(zt_t t, zt_test_runner* runner) +{ + for (size_t i = runner->resources.defer_len; i > 0; --i) { + zt_invoke_closure(t, &runner->resources.defer_closures[i - 1]); + } + runner->resources.defer_len = 0; +} + +static void zt_runner_visitor__visit_case(void* id, zt_test_case_func test_func, const char* name) { zt_test_runner* runner = (zt_test_runner*)id; zt_test test; int jump_result; memset(&test, 0, sizeof test); - test.stream = runner->stream_err; test.outcome = ZT_PENDING; + zt_provide_resources_to_test(runner, &test); #if defined(_WIN32) || defined(__WATCOMC__) jump_result = setjmp(test.jump_buffer); #else @@ -418,8 +524,10 @@ static void zt_runner_visitor__visit_case(void* id, zt_test_case_func func, if (runner->verbose && runner->stream_out) { fprintf(runner->stream_out, "%*c %s", runner->nesting * 3, '-', name); } - func(&test); + test_func(&test); } + zt_run_defer_funcs(&test, runner); + switch (test.outcome) { case ZT_PENDING: case ZT_PASSED: @@ -447,9 +555,32 @@ static void zt_runner_visitor__visit_case(void* id, zt_test_case_func func, } } +static void zt_runner_visitor__visit_resource(void* id, zt_resource_kind kind, size_t count, void* data) +{ + zt_test_runner* runner = (zt_test_runner*)id; + switch (kind) { + case ZT_RESOURCE_DEBUG_STREAM: + runner->resources.debug_stream = data; + break; + case ZT_RESOURCE_DEFER_CLOSURES: + runner->resources.defer_closures = data; + runner->resources.defer_cap = count; + runner->resources.defer_len = 0; + break; + default: + if (runner->stream_err) { + fprintf(runner->stream_err, "%*c - unexpected resource code %d\n", + runner->nesting * 3, '-', kind); + } + runner->num_failed++; + break; + } +} + static const zt_visitor_vtab zt_test_runner__visitor_vtab = { /* .visit_case = */ zt_runner_visitor__visit_case, /* .visit_suite = */ zt_runner_visitor__visit_suite, + /* .visit_resource = */ zt_runner_visitor__visit_resource, }; static zt_visitor zt_visitor_from_test_runner(zt_test_runner* runner) @@ -542,6 +673,7 @@ void zt_assert(zt_test* test, zt_claim claim) siglongjmp(test->jump_buffer, 1); #endif /* TODO: in C++ mode throw an exception. */ + zt_nonlocal_exit(test, 1); } } diff --git a/zt.h b/zt.h index bc6f205..2d22948 100644 --- a/zt.h +++ b/zt.h @@ -37,6 +37,11 @@ typedef struct zt_visitor { const struct zt_visitor_vtab* vtab; } zt_visitor; +typedef enum zt_resource_kind { + ZT_RESOURCE_DEBUG_STREAM, + ZT_RESOURCE_DEFER_CLOSURES, +} zt_resource_kind; + typedef void (*zt_test_case_func)(zt_t); typedef void (*zt_test_suite_func)(zt_visitor); @@ -44,6 +49,7 @@ int zt_main(int argc, char** argv, char** envp, zt_test_suite_func tsuite); void zt_visit_test_suite(zt_visitor v, zt_test_suite_func func, const char* name); void zt_visit_test_case(zt_visitor v, zt_test_case_func func, const char* name); +void zt_visit_resource(zt_visitor v, zt_resource_kind kind, size_t count, void* data); #define ZT_VISIT_TEST_SUITE(v, tsuite) zt_visit_test_suite(v, tsuite, #tsuite) #define ZT_VISIT_TEST_CASE(v, tcase) zt_visit_test_case(v, tcase, #tcase) @@ -146,6 +152,47 @@ static inline zt_location zt_location_at(const char* fname, int lineno) #define ZT_CURRENT_LOCATION() zt_location_at(__FILE__, __LINE__) +typedef void (*zt_closure_func0)(void); +typedef void (*zt_closure_func1)(zt_value arg1); + +typedef struct zt_closure { + union { + zt_closure_func0 args0; + zt_closure_func1 args1; + } func; + zt_location location; + int nargs; + zt_value args[1]; +} zt_closure; + +static inline zt_closure zt_pack_closure0(zt_location location, zt_closure_func0 func) +{ + zt_closure c; + c.func.args0 = func; + c.location = location; + c.nargs = 0; + c.args[0] = zt_pack_nothing(); + return c; +} + +static inline zt_closure zt_pack_closure1(zt_location location, zt_closure_func1 func, zt_value arg1) +{ + zt_closure c; + c.func.args1 = func; + c.location = location; + c.nargs = 1; + c.args[0] = arg1; + return c; +} + +void zt_defer(zt_t test, zt_closure closure); + +#define ZT_CLOSURE0(func) \ + zt_pack_closure0(ZT_CURRENT_LOCATION(), (func)) + +#define ZT_CLOSURE1(func, arg1) \ + zt_pack_closure1(ZT_CURRENT_LOCATION(), (func), (arg1)) + struct zt_verifier; typedef struct zt_claim { struct zt_verifier (*make_verifier)(void);