diff --git a/TODO b/TODO index 1554b51..e69de29 100644 --- a/TODO +++ b/TODO @@ -1 +0,0 @@ -Implement chunked reading diff --git a/include/rjp.h b/include/rjp.h index 72f2cee..90f0aaf 100644 --- a/include/rjp.h +++ b/include/rjp.h @@ -89,6 +89,10 @@ typedef enum RJP_parse_flag{ RJP_PARSE_ALLOW_COMMENTS = 1, RJP_PARSE_ALLOW_TRAILING_COMMA = 2 }RJP_parse_flag; +typedef struct RJP_parse_callback{ + int(*read)(char*,int,void*); + void* data; +}RJP_parse_callback; /***************** NON OBJECT OPERATIONS *******************/ @@ -109,7 +113,7 @@ RJP_index rjp_escape_strlen(const char* str); /***************** GENERIC OPERATIONS *******************/ //Convert C string consisting of json data into RJP's format RJP_value* rjp_parse(const char* str, int flags); -//RJP_value* rjp_parse_chunked(const char* str, RJP_value* prev_chunk); +RJP_value* rjp_parse_cback(int flags, RJP_parse_callback* cbacks); char* rjp_to_json(const RJP_value* root, int pretty); //Initialize a root RJP_value to copy of value diff --git a/include/rjp_lex.h b/include/rjp_lex.h index 1bdb28b..92ee81e 100644 --- a/include/rjp_lex.h +++ b/include/rjp_lex.h @@ -21,6 +21,9 @@ #include "rjp.h" +#define RJP_LEX_CBACK_BUFFER_SIZE 512 +#define RJP_LEX_CBACK_STR_SIZE 64 + #define rjp_lex_accept 1 typedef enum RJP_lex_category{ rjp_lex_start = 0, @@ -66,12 +69,19 @@ typedef enum RJP_lex_category{ }RJP_lex_category; typedef struct RJP_lex_state{ - const char* str; + char* str; + char* buff; + RJP_index strl; + RJP_index buffl; + RJP_index buffcap; + RJP_index buffpos; RJP_lex_category node; RJP_index length; RJP_index offset; }RJP_lex_state; +void irjp_init_lex_cback_state(RJP_lex_state* state); RJP_lex_category irjp_lex(RJP_lex_state* state); +RJP_lex_category irjp_lex_cback(RJP_lex_state* state, RJP_parse_callback* cbacks); #endif diff --git a/src/rjp_lex.c b/src/rjp_lex.c index e1df9c1..1357a15 100644 --- a/src/rjp_lex.c +++ b/src/rjp_lex.c @@ -19,7 +19,15 @@ #include "rjp_lex.h" #include "rjp.h" #include //isalpha, etc +#include //memcpy +void irjp_init_lex_cback_state(RJP_lex_state* state){ + state->str = rjp_alloc(RJP_LEX_CBACK_STR_SIZE+1); + state->str[RJP_LEX_CBACK_STR_SIZE] = 0; + state->strl = RJP_LEX_CBACK_STR_SIZE; + state->buff = rjp_alloc(RJP_LEX_CBACK_BUFFER_SIZE); + state->buffcap = RJP_LEX_CBACK_BUFFER_SIZE; +} static RJP_lex_category irjp_lex_accept(RJP_lex_category val, RJP_lex_state* state){ state->node = rjp_lex_start; return val; @@ -297,3 +305,43 @@ RJP_lex_category irjp_lex(RJP_lex_state* state){ } return irjp_lex_accept(state->node, state); } + +static void irjp_lex_resize_strbuf(RJP_lex_state* state, int newsize){ + char* newbuf = rjp_alloc(newsize+1); + memcpy(newbuf, state->str, state->strl); + newbuf[newsize] = 0; + rjp_free(state->str); + state->str = newbuf; + state->strl = newsize; +} +RJP_lex_category irjp_lex_cback(RJP_lex_state* state, RJP_parse_callback* cbacks){ + state->length = 0; + RJP_index chars_read = state->buffl; + if(chars_read == 0){ + state->buffpos = 0; + chars_read = cbacks->read(state->buff, state->buffcap, cbacks->data); + state->buffl = chars_read; + } + while(chars_read > 0){ + for(RJP_index i = 0;(i+state->buffpos) < chars_read;++i,++state->length){ + if(state->length == state->strl) + irjp_lex_resize_strbuf(state, state->strl*2); + RJP_lex_category cat = irjp_lex_char(state->buff[state->buffpos+i], state->node); + if(cat == rjp_lex_invalid){ + state->buffpos = i + state->buffpos; + state->str[state->length] = 0; + return irjp_lex_accept(state->node, state); + } + state->str[state->length] = state->buff[state->buffpos+i]; + state->node = cat; + } + chars_read = cbacks->read(state->buff, state->buffcap, cbacks->data); + state->buffpos = 0; + state->buffl = chars_read; + } + ++state->buffpos; + state->str[state->length] = 0; + RJP_lex_category cat = state->node; + state->node = rjp_lex_end; + return cat; +} diff --git a/src/rjp_parse.c b/src/rjp_parse.c index 7ccaec2..01a9c85 100644 --- a/src/rjp_parse.c +++ b/src/rjp_parse.c @@ -164,7 +164,7 @@ static void irjp_init_parse_state(RJP_parse_state* state, const char* str){ state->row = 1; irjp_init_parse_stack(&state->target_stack); - state->lexstate.str = str; + state->lexstate.str = (char*)str; state->root = state->curr = state->lastadded = rjp_calloc(1, sizeof(RJP_value)); } static void irjp_delete_parse_state(RJP_parse_state* state){ @@ -284,6 +284,18 @@ static int irjp_parse(RJP_parse_state* state){ return RJP_PARSE_STATUS_SUC; irjp_parse_error("Invalid Token"); } +static int irjp_parse_cback(RJP_parse_state* state, RJP_parse_callback* cback){ + RJP_lex_category cat; + for(cat = irjp_lex_cback(&state->lexstate, cback);cat & rjp_lex_accept;cat = irjp_lex_cback(&state->lexstate, cback),state->row += state->lexstate.length){ + if(irjp_parse_handle_lexcat(cat, state) != RJP_PARSE_STATUS_SUC) + return RJP_PARSE_STATUS_ERR; + } + if(state->target_stack.position != 0) + irjp_parse_error("Missing closing brace"); + if(cat == rjp_lex_end) + return RJP_PARSE_STATUS_SUC; + irjp_parse_error("Invalid Token"); +} RJP_value* rjp_parse(const char* str, int flags){ RJP_parse_state state = {.allow_comments = (flags & RJP_PARSE_ALLOW_COMMENTS), .allow_trail_comma = (flags & RJP_PARSE_ALLOW_TRAILING_COMMA) @@ -299,9 +311,20 @@ RJP_value* rjp_parse(const char* str, int flags){ } } -RJP_value* rjp_parse_chunked(const char* str, RJP_value* prev_chunk){ - if(!prev_chunk){ - return rjp_parse(str, RJP_PARSE_NONE); +RJP_value* rjp_parse_cback(int flags, RJP_parse_callback* cback){ + RJP_parse_state state = {.allow_comments = (flags & RJP_PARSE_ALLOW_COMMENTS), + .allow_trail_comma = (flags & RJP_PARSE_ALLOW_TRAILING_COMMA) + }; + irjp_init_parse_state(&state, NULL); + irjp_init_lex_cback_state(&state.lexstate); + int status = irjp_parse_cback(&state, cback); + rjp_free(state.lexstate.str); + rjp_free(state.lexstate.buff); + if(status == RJP_PARSE_STATUS_SUC){ + irjp_delete_parse_stack(&state.target_stack); + return state.root; + }else{ + irjp_delete_parse_state(&state); + return NULL; } - return NULL; } diff --git a/src/test/test.c b/src/test/test.c index 040940d..ad6815b 100644 --- a/src/test/test.c +++ b/src/test/test.c @@ -3,10 +3,29 @@ #include "rjp_string.h" #include +#include -int test(const char* str, RJP_parse_flag flags){ - RJP_value* res; - res = rjp_parse(str, flags); +#define MIN(x, y) ((x < y) ? (x) : (y)) + +typedef struct parse_stuff{ + const char* str; + int len; + int pos; +}parse_stuff; + +int read_callback(char* c, int size, void* userdata){ + parse_stuff* data = (parse_stuff*)userdata; + if(data->pos >= data->len) + return 0; + int min = MIN(size, data->len - data->pos); + int i; + for(i = 0;i < min;++i){ + c[i] = data->str[data->pos++]; + } + return i; +} + +int handle_res(RJP_value* res){ if(res){ fprintf(stderr, "Accepted\n"); } @@ -19,123 +38,141 @@ int test(const char* str, RJP_parse_flag flags){ rjp_free_value(res); return failed; } +int test_cbacks(const char* str, RJP_parse_flag flags){ + RJP_parse_callback cbacks; + parse_stuff cback_data = {str, strlen(str), 0}; + cbacks.read = read_callback; + cbacks.data = &cback_data; + RJP_value* res; + res = rjp_parse_cback(flags, &cbacks); + return handle_res(res); +} + +int test(const char* str, RJP_parse_flag flags){ + RJP_value* res; + res = rjp_parse(str, flags); + return handle_res(res); +} struct parse_pair{ const char* str; RJP_parse_flag flags; }; -int main(){ - struct parse_pair should_pass_strings[] = { - {"{}", RJP_PARSE_NONE}, - {"[]", RJP_PARSE_NONE}, - {"\"s\"", RJP_PARSE_NONE}, - {"\"\\n\"", RJP_PARSE_NONE}, - {"\"\\\"\"", RJP_PARSE_NONE}, - {"\"str\\nstr\"", RJP_PARSE_NONE}, - {"\"\\uD83D\\uDE10\"", RJP_PARSE_NONE}, - {"true", RJP_PARSE_NONE}, - {"false", RJP_PARSE_NONE}, - {"null", RJP_PARSE_NONE}, - {"5", RJP_PARSE_NONE}, - {"-5", RJP_PARSE_NONE}, - {"+5", RJP_PARSE_NONE}, - {"5.5", RJP_PARSE_NONE}, - {"-5.5", RJP_PARSE_NONE}, - {"+5.5", RJP_PARSE_NONE}, - {"5.5e6", RJP_PARSE_NONE}, - {"-5.5e6", RJP_PARSE_NONE}, - {"+5.5e6", RJP_PARSE_NONE}, - {"5.5e+6", RJP_PARSE_NONE}, - {"-5.5e+6", RJP_PARSE_NONE}, - {"+5.5e+6", RJP_PARSE_NONE}, - {"5.5e-6", RJP_PARSE_NONE}, - {"-5.5e-6", RJP_PARSE_NONE}, - {"+5.5e-6", RJP_PARSE_NONE}, - {" {}", RJP_PARSE_NONE}, - {"\n{}\n", RJP_PARSE_NONE}, - {" { \"key\" \t:\n\n\n5 \n\t\n } ", RJP_PARSE_NONE}, - {" {\t }\n", RJP_PARSE_NONE}, - {"5.5 ", RJP_PARSE_NONE}, - {"{\"key\":5}", RJP_PARSE_NONE}, - {"{\"key\":{}}", RJP_PARSE_NONE}, - {"{\"\\uD83D\\uDE10\":5}", RJP_PARSE_NONE}, - {"{\"😐\":5}", RJP_PARSE_NONE}, - {"{\"key\":{\"key\":5}}", RJP_PARSE_NONE}, - {"{\"key\":{\"key\":5,\"key2\":6}}", RJP_PARSE_NONE}, - {"{\"key\":{\"key\":5},\"key2\":6}", RJP_PARSE_NONE}, - {"[5, 6, 7, 8, 9, \"10\"]", RJP_PARSE_NONE}, - {"[[5,6],[7,8],[9,\"10\"]]", RJP_PARSE_NONE}, - {"{\"arr\":[5,6,6]}", RJP_PARSE_NONE}, - {"[{\"arr\":[5,6,6]}]", RJP_PARSE_NONE}, - {"[{\"arr\":[5,6,6]}, 6]", RJP_PARSE_NONE}, - {"[5,6,6,6,6.6]", RJP_PARSE_NONE}, - {"[6,7,]", RJP_PARSE_ALLOW_TRAILING_COMMA}, - {"{\"1\":1,\"2\":2,}", RJP_PARSE_ALLOW_TRAILING_COMMA}, - {"[6,]", RJP_PARSE_ALLOW_TRAILING_COMMA}, - {"{\"1\":1,}", RJP_PARSE_ALLOW_TRAILING_COMMA}, - {"//comment\n{}", RJP_PARSE_ALLOW_COMMENTS}, - {"{\"key\"://comment\n5}", RJP_PARSE_ALLOW_COMMENTS}, - {"{\"key\"//comment\n:5}", RJP_PARSE_ALLOW_COMMENTS}, - {"{}//comment", RJP_PARSE_ALLOW_COMMENTS}, - {"{//\"key\":5\n}", RJP_PARSE_ALLOW_COMMENTS}, - {"5 //comment*/", RJP_PARSE_ALLOW_COMMENTS}, - {"{/*\"key\":5*/\"key\":5}", RJP_PARSE_ALLOW_COMMENTS}, - {"[5, /*comment*/6]", RJP_PARSE_ALLOW_COMMENTS}, - }; - const int should_pass_cnt = sizeof(should_pass_strings)/sizeof(should_pass_strings[0]); - struct parse_pair should_fail_strings[] = { - {"//comment\n{}", RJP_PARSE_NONE}, - {"{", RJP_PARSE_NONE}, - {"}", RJP_PARSE_NONE}, - {"[", RJP_PARSE_NONE}, - {"]", RJP_PARSE_NONE}, - {"6.", RJP_PARSE_NONE}, - {"6.6e", RJP_PARSE_NONE}, - {"6.6e+", RJP_PARSE_NONE}, - {"{6}", RJP_PARSE_NONE}, - {"[\"key\":5]", RJP_PARSE_NONE}, - {"\"string\n\"", RJP_PARSE_NONE}, - {"[3 4]", RJP_PARSE_NONE}, - {"\"\\uD83D\\uDE1\"", RJP_PARSE_NONE}, - {"\"\\uD83D\\uDE1Q\"", RJP_PARSE_NONE}, - {"\"\\uD83\\uDE10\"", RJP_PARSE_NONE}, - {"\"\\uF83D\\uDE10\"", RJP_PARSE_NONE}, - {"\"\\uU83D\\uDE10\"", RJP_PARSE_NONE}, - {"{\"key\":1 \"key2\":2}", RJP_PARSE_NONE}, - {"{\"key\" 1}", RJP_PARSE_NONE}, - {"6, 7", RJP_PARSE_NONE}, - {"[,]", RJP_PARSE_NONE}, - {"{, RJP_PARSE_NONE}", RJP_PARSE_NONE}, - {"[1, 2],", RJP_PARSE_NONE}, - {"{\"key\nkey\":5}", RJP_PARSE_NONE}, - {"{\"key\":\"key\n\"}", RJP_PARSE_NONE}, - {"[6,7,]", RJP_PARSE_NONE}, - {"{\"1\":1,\"2\":2, RJP_PARSE_NONE}", RJP_PARSE_NONE}, - {"[6,]", RJP_PARSE_NONE}, - {"{\"1\":1, RJP_PARSE_NONE}", RJP_PARSE_NONE}, - {"{//comment\"key\":\n5}", RJP_PARSE_NONE}, - {"{/*\"key\":*/5}", RJP_PARSE_NONE}, - {"[5, /*6*/, 7]", RJP_PARSE_NONE}, - {"{/*comment}", RJP_PARSE_NONE}, - {"{//comment}", RJP_PARSE_NONE}, - {"{\"key\"://comment\n5}", RJP_PARSE_NONE}, - {"{\"key\"//comment\n:5}", RJP_PARSE_NONE}, - {"{}//comment", RJP_PARSE_NONE}, - {"{//\"key\":5\n}", RJP_PARSE_NONE}, - {"5 //comment*/", RJP_PARSE_NONE}, - {"{/*\"key\":5*/\"key\":5}", RJP_PARSE_NONE}, - {"[5, /*comment*/6]", RJP_PARSE_NONE}, - }; - const int should_fail_cnt = sizeof(should_fail_strings)/sizeof(should_fail_strings[0]); - const int total_tests = should_pass_cnt + should_fail_cnt; +struct parse_pair should_pass_strings[] = { + {"{}", RJP_PARSE_NONE}, + {"[]", RJP_PARSE_NONE}, + {"\"s\"", RJP_PARSE_NONE}, + {"\"\\n\"", RJP_PARSE_NONE}, + {"\"\\\"\"", RJP_PARSE_NONE}, + {"\"str\\nstr\"", RJP_PARSE_NONE}, + {"\"\\uD83D\\uDE10\"", RJP_PARSE_NONE}, + {"true", RJP_PARSE_NONE}, + {"false", RJP_PARSE_NONE}, + {"null", RJP_PARSE_NONE}, + {"5", RJP_PARSE_NONE}, + {"-5", RJP_PARSE_NONE}, + {"+5", RJP_PARSE_NONE}, + {"5.5", RJP_PARSE_NONE}, + {"-5.5", RJP_PARSE_NONE}, + {"+5.5", RJP_PARSE_NONE}, + {"5.5e6", RJP_PARSE_NONE}, + {"-5.5e6", RJP_PARSE_NONE}, + {"+5.5e6", RJP_PARSE_NONE}, + {"5.5e+6", RJP_PARSE_NONE}, + {"-5.5e+6", RJP_PARSE_NONE}, + {"+5.5e+6", RJP_PARSE_NONE}, + {"5.5e-6", RJP_PARSE_NONE}, + {"-5.5e-6", RJP_PARSE_NONE}, + {"+5.5e-6", RJP_PARSE_NONE}, + {" {}", RJP_PARSE_NONE}, + {"\n{}\n", RJP_PARSE_NONE}, + {" { \"key\" \t:\n\n\n5 \n\t\n } ", RJP_PARSE_NONE}, + {" {\t }\n", RJP_PARSE_NONE}, + {"5.5 ", RJP_PARSE_NONE}, + {"{\"key\":5}", RJP_PARSE_NONE}, + {"{\"key\":{}}", RJP_PARSE_NONE}, + {"{\"\\uD83D\\uDE10\":5}", RJP_PARSE_NONE}, + {"{\"😐\":5}", RJP_PARSE_NONE}, + {"{\"key\":{\"key\":5}}", RJP_PARSE_NONE}, + {"{\"key\":{\"key\":5,\"key2\":6}}", RJP_PARSE_NONE}, + {"{\"key\":{\"key\":5},\"key2\":6}", RJP_PARSE_NONE}, + {"[5, 6, 7, 8, 9, \"10\"]", RJP_PARSE_NONE}, + {"[[5,6],[7,8],[9,\"10\"]]", RJP_PARSE_NONE}, + {"{\"arr\":[5,6,6]}", RJP_PARSE_NONE}, + {"[{\"arr\":[5,6,6]}]", RJP_PARSE_NONE}, + {"[{\"arr\":[5,6,6]}, 6]", RJP_PARSE_NONE}, + {"[5,6,6,6,6.6]", RJP_PARSE_NONE}, + {"[6,7,]", RJP_PARSE_ALLOW_TRAILING_COMMA}, + {"{\"1\":1,\"2\":2,}", RJP_PARSE_ALLOW_TRAILING_COMMA}, + {"[6,]", RJP_PARSE_ALLOW_TRAILING_COMMA}, + {"{\"1\":1,}", RJP_PARSE_ALLOW_TRAILING_COMMA}, + {"//comment\n{}", RJP_PARSE_ALLOW_COMMENTS}, + {"{\"key\"://comment\n5}", RJP_PARSE_ALLOW_COMMENTS}, + {"{\"key\"//comment\n:5}", RJP_PARSE_ALLOW_COMMENTS}, + {"{}//comment", RJP_PARSE_ALLOW_COMMENTS}, + {"{//\"key\":5\n}", RJP_PARSE_ALLOW_COMMENTS}, + {"5 //comment*/", RJP_PARSE_ALLOW_COMMENTS}, + {"{/*\"key\":5*/\"key\":5}", RJP_PARSE_ALLOW_COMMENTS}, + {"[5, /*comment*/6]", RJP_PARSE_ALLOW_COMMENTS}, +}; +const int should_pass_cnt = sizeof(should_pass_strings)/sizeof(should_pass_strings[0]); +struct parse_pair should_fail_strings[] = { + {"//comment\n{}", RJP_PARSE_NONE}, + {"{", RJP_PARSE_NONE}, + {"}", RJP_PARSE_NONE}, + {"[", RJP_PARSE_NONE}, + {"]", RJP_PARSE_NONE}, + {"6.", RJP_PARSE_NONE}, + {"6.6e", RJP_PARSE_NONE}, + {"6.6e+", RJP_PARSE_NONE}, + {"5e", RJP_PARSE_NONE}, + {"{6}", RJP_PARSE_NONE}, + {"[\"key\":5]", RJP_PARSE_NONE}, + {"\"string\n\"", RJP_PARSE_NONE}, + {"[3 4]", RJP_PARSE_NONE}, + {"\"\\uD83D\\uDE1\"", RJP_PARSE_NONE}, + {"\"\\uD83D\\uDE1Q\"", RJP_PARSE_NONE}, + {"\"\\uD83\\uDE10\"", RJP_PARSE_NONE}, + {"\"\\uF83D\\uDE10\"", RJP_PARSE_NONE}, + {"\"\\uU83D\\uDE10\"", RJP_PARSE_NONE}, + {"{\"key\":1 \"key2\":2}", RJP_PARSE_NONE}, + {"{\"key\" 1}", RJP_PARSE_NONE}, + {"6, 7", RJP_PARSE_NONE}, + {"[,]", RJP_PARSE_NONE}, + {"{, RJP_PARSE_NONE}", RJP_PARSE_NONE}, + {"[1, 2],", RJP_PARSE_NONE}, + {"{\"key\nkey\":5}", RJP_PARSE_NONE}, + {"{\"key\":\"key\n\"}", RJP_PARSE_NONE}, + {"[6,7,]", RJP_PARSE_NONE}, + {"{\"1\":1,\"2\":2, RJP_PARSE_NONE}", RJP_PARSE_NONE}, + {"[6,]", RJP_PARSE_NONE}, + {"{\"1\":1, RJP_PARSE_NONE}", RJP_PARSE_NONE}, + {"{//comment\"key\":\n5}", RJP_PARSE_NONE}, + {"{/*\"key\":*/5}", RJP_PARSE_NONE}, + {"[5, /*6*/, 7]", RJP_PARSE_NONE}, + {"{/*comment}", RJP_PARSE_NONE}, + {"{//comment}", RJP_PARSE_NONE}, + {"{\"key\"://comment\n5}", RJP_PARSE_NONE}, + {"{\"key\"//comment\n:5}", RJP_PARSE_NONE}, + {"{}//comment", RJP_PARSE_NONE}, + {"{//\"key\":5\n}", RJP_PARSE_NONE}, + {"5 //comment*/", RJP_PARSE_NONE}, + {"{/*\"key\":5*/\"key\":5}", RJP_PARSE_NONE}, + {"[5, /*comment*/6]", RJP_PARSE_NONE}, + {"{\"key\"//:5}", RJP_PARSE_ALLOW_COMMENTS}, + {"{,}", RJP_PARSE_ALLOW_TRAILING_COMMA}, + {"[,]", RJP_PARSE_ALLOW_TRAILING_COMMA}, +}; +const int should_fail_cnt = sizeof(should_fail_strings)/sizeof(should_fail_strings[0]); +const int total_tests = should_pass_cnt + should_fail_cnt; +int run_test(int (*fun)(const char*,RJP_parse_flag)){ int passed = 0; - fprintf(stderr, "Running %d tests that should pass...\n", should_pass_cnt); for(unsigned i = 0;i < sizeof(should_pass_strings)/sizeof(should_pass_strings[0]);++i){ fprintf(stderr, "%8d) ", i+1); - if(!test(should_pass_strings[i].str, should_pass_strings[i].flags)){ + if(!fun(should_pass_strings[i].str, should_pass_strings[i].flags)){ ++passed; }else{ fprintf(stderr, "%13s%s\n", "", should_pass_strings[i].str); @@ -145,11 +182,21 @@ int main(){ printf("Running %d tests that should fail...\n", should_fail_cnt); for(unsigned i = 0;i < sizeof(should_fail_strings)/sizeof(should_fail_strings[0]);++i){ fprintf(stderr, "%8d) ", i+1); - if(test(should_fail_strings[i].str, should_fail_strings[i].flags)){ + if(fun(should_fail_strings[i].str, should_fail_strings[i].flags)){ ++passed; }else{ fprintf(stderr, "%13s%s\n", "", should_fail_strings[i].str); } } - fprintf(stderr, "\nResults: %d/%d tests passed\n", passed, total_tests); + return passed; +} +int main(){ + + int normal_passed = run_test(test); + int cback_passed = run_test(test_cbacks); + int total_passed = normal_passed + cback_passed; + + fprintf(stderr, "\nResults: %d/%d normal tests passed\n", normal_passed, total_tests); + fprintf(stderr, "Results: %d/%d callback tests passed\n", cback_passed, total_tests); + fprintf(stderr, "Results: %d/%d tests passed\n", total_passed, total_tests*2); }