diff --git a/requesters/Makefile b/requesters/Makefile index d5f02f1..3d6870e 100644 --- a/requesters/Makefile +++ b/requesters/Makefile @@ -7,5 +7,12 @@ all: main clean: rm -f *.o main -main: main.o filereq.o dos13.o +main: main.o filereq.o dos13.o file_list.o $(CC) $(CFLAGS) $^ -lamiga -lauto -o $@ + + +check: file_list_test + +file_list_test: file_list.c file_list_test.c chibi.c + gcc $^ -o $@ + ./file_list_test diff --git a/requesters/chibi.c b/requesters/chibi.c new file mode 100644 index 0000000..f31f41f --- /dev/null +++ b/requesters/chibi.c @@ -0,0 +1,336 @@ +#include +#include +#include + +#include "chibi.h" + +#define MAX_DIGITS_INT 10 + +chibi_suite *chibi_suite_new_fixture(chibi_fixfunc setup, + chibi_fixfunc teardown, + void *userdata) +{ + chibi_suite *result = calloc(1, sizeof(chibi_suite)); + result->head = NULL; + result->setup = setup; + result->teardown = teardown; + result->userdata = userdata; + result->first_child = NULL; + result->next = NULL; + return result; +} + +chibi_suite *chibi_suite_new() +{ + return chibi_suite_new_fixture(NULL, NULL, NULL); +} + +void chibi_suite_delete(chibi_suite *suite) +{ + if (suite) { + struct _chibi_testcase *tc = suite->head, *tmp; + while (tc) { + tmp = tc->next; + if (tc->error_msg) free(tc->error_msg); + free(tc); + tc = tmp; + } + /* recursively free the children and siblings */ + if (suite->first_child) chibi_suite_delete(suite->first_child); + if (suite->next) chibi_suite_delete(suite->next); + free(suite); + } +} + +void chibi_suite_add_suite(chibi_suite *suite, chibi_suite *toadd) +{ + if (!suite->first_child) suite->first_child = toadd; + else { + chibi_suite *cur = suite->first_child; + while (cur->next) cur = cur->next; + cur->next = toadd; + } +} + +void _chibi_suite_add_test(chibi_suite *suite, chibi_testfunc fun, const char *fname) +{ + struct _chibi_testcase *tc, *newtc; + newtc = calloc(1, sizeof(struct _chibi_testcase)); + newtc->fun = fun; + newtc->fname = fname; + newtc->next = NULL; + newtc->success = 1; + newtc->error_msg = NULL; + newtc->userdata = suite->userdata; + + if (!suite->head) suite->head = newtc; + else { + tc = suite->head; + while (tc->next) tc = tc->next; + tc->next = newtc; + } +} + +static void _chibi_suite_summary_data(chibi_suite *suite, chibi_summary_data *summary, int level) +{ + if (suite && summary) { + chibi_testcase *tc = suite->head; + if (!level) { + summary->num_runs = 0; + summary->num_failures = 0; + summary->num_pass = 0; + } + + while (tc) { + summary->num_runs++; + if (!tc->success) { + summary->num_failures++; + } + tc = tc->next; + } + if (suite->first_child) _chibi_suite_summary_data(suite->first_child, summary, level + 1); + if (suite->next) _chibi_suite_summary_data(suite->next, summary, level + 1); + + if (!level) summary->num_pass = summary->num_runs - summary->num_failures; + } +} + +static int _print_messages(chibi_suite *suite, int testnum) +{ + chibi_testcase *tc = suite->head; + while (tc) { + if (!tc->success) { + fprintf(stderr, "%d. %s\n", testnum, tc->error_msg); + testnum++; + } + tc = tc->next; + } + if (suite->first_child) testnum = _print_messages(suite->first_child, testnum); + if (suite->next) testnum = _print_messages(suite->next, testnum); + + return testnum; +} + +static void chibi_suite_print_summary(chibi_suite *suite) +{ + chibi_summary_data summary; + _chibi_suite_summary_data(suite, &summary, 0); + + fprintf(stderr, "\n\nSummary (chibitest %s)\n\n", CHIBI_TEST_GIT_SHA); + if (summary.num_failures > 0) { + fprintf(stderr, "# of failures: %d\n\n", summary.num_failures); + _print_messages(suite, 0); + fprintf(stderr, "\n"); + } + fprintf(stderr, "Runs: %d Pass: %d Fail: %d\n\n", summary.num_runs, + summary.num_runs - summary.num_failures, summary.num_failures); +} + +/* + * Reused by assertions to generate a standard format error message. + */ +static char *assemble_message(const char *msg, const char *srcfile, + const char *funname, int line) +{ + char *msgbuffer = calloc(strlen(msg) + strlen(srcfile) + strlen(funname) + MAX_DIGITS_INT + 8, + sizeof(char)); + sprintf(msgbuffer, "%s:%d - %s() - %s", srcfile, line, funname, msg); + return msgbuffer; +} + +static char *assemble_message2(const char *msg1, const char *msg2, + const char *srcfile, const char *funname, + int line) +{ + char *msgbuffer = calloc(strlen(msg1) + strlen(msg2) + strlen(srcfile) + strlen(funname) + + MAX_DIGITS_INT + 10, sizeof(char)); + sprintf(msgbuffer, "%s:%d - %s() - %s %s", srcfile, line, funname, msg1, msg2); + return msgbuffer; +} + +/********************************************************************** + * + * ASSERTIONS + * + * TODO: test exit after first fail: make a test case with 2 assertions and ensure + * that the second is not executed when the first fails + * Note: setjmp()/longjmp() seem to be broken on Amiga/VBCC + * + **********************************************************************/ + +void _exit_on_fail(chibi_testcase *tc) +{ +#ifndef AMIGA + longjmp(tc->env, 0); +#endif +} + +void _chibi_assert_not_null(chibi_testcase *tc, void *ptr, const char *msg, const char *srcfile, + int line) +{ + if (ptr == NULL) { + tc->error_msg = assemble_message(msg, srcfile, tc->fname, line); + tc->success = 0; + _exit_on_fail(tc); + } +} + +void _chibi_fail(chibi_testcase *tc, const char *msg, const char *srcfile, int line) +{ + tc->error_msg = assemble_message(msg, srcfile, tc->fname, line); + tc->success = 0; + _exit_on_fail(tc); +} + +void _chibi_assert(chibi_testcase *tc, int cond, const char *cond_str, const char *msg, + const char *srcfile, int line) +{ + if (!cond) { + tc->error_msg = assemble_message2(msg, cond_str, srcfile, tc->fname, line); + tc->success = 0; + _exit_on_fail(tc); + } +} + +void _chibi_assert_eq_int(chibi_testcase *tc, int expected, int value, + const char *srcfile, int line) +{ + if (value != expected) { + char *fmt = "%s:%d - %s() - expected:<%d> but was:<%d>"; + char *msgbuffer = calloc(strlen(fmt) + strlen(srcfile) + strlen(tc->fname) + + MAX_DIGITS_INT * 3 + 10, sizeof(char)); + sprintf(msgbuffer, fmt, srcfile, line, tc->fname, expected, value); + tc->error_msg = msgbuffer; + tc->success = 0; + _exit_on_fail(tc); + } +} + +void _chibi_assert_eq_cstr(chibi_testcase *tc, const char *expected, const char *value, + const char *srcfile, int line) +{ + if (value == expected) return; + if (!value || !expected || strcmp(value, expected)) { + char *fmt, *msgbuffer; + + if (!expected) expected = "(null)"; + if (!value) value = "(null)"; + fmt = "%s:%d - %s() - expected:<%s> but was:<%s>"; + msgbuffer = calloc(strlen(fmt) + strlen(srcfile) + strlen(tc->fname) + + strlen(expected) + strlen(value) + + MAX_DIGITS_INT + 10, sizeof(char)); + sprintf(msgbuffer, fmt, srcfile, line, tc->fname, expected, value); + tc->error_msg = msgbuffer; + tc->success = 0; + _exit_on_fail(tc); + } +} + +/********************************************************************** + * + * TEST RUNNERS + * + **********************************************************************/ + +/* + * Generic runner. Supports fixtures and report function customization. + * If the suite was defined with setup and/or teardown functions, those + * are run on the optional userdata object. + * The report_num_tests(int), report_success(int, chibi_testcase *) and + * report_fail(int, chibi_testcase *) functions are used to support + * different output protocols (e.g. for reporting the success/failure of + * tests while they are run). + */ +static int _count_tests(chibi_suite *suite) { + chibi_testcase *testcase = suite->head; + int result = 0; + if (suite->first_child) result += _count_tests(suite->first_child); + if (suite->next) result += _count_tests(suite->next); + while (testcase) { + result++; + testcase = testcase->next; + } + return result; +} + +static int _chibi_suite_run(chibi_suite *suite, void (*report_num_tests)(int), + void (*report_success)(int, chibi_testcase *), + void (*report_fail)(int, chibi_testcase *), + int tcnum, int level) +{ + if (suite) { + chibi_testcase *testcase; + + if (suite->first_child) { + tcnum = _chibi_suite_run(suite->first_child, report_num_tests, + report_success, report_fail, tcnum, level + 1); + } + if (suite->next) { + tcnum = _chibi_suite_run(suite->next, report_num_tests, + report_success, report_fail, tcnum, level + 1); + } + + /* only report the number of tests at the top level */ + if (level == 0) report_num_tests(_count_tests(suite)); + + /* run this level's tests */ + testcase = suite->head; + while (testcase) { +#ifndef AMIGA + if (!setjmp(testcase->env)) { +#endif + if (suite->setup) suite->setup(suite->userdata); + testcase->fun(testcase); + if (suite->teardown) suite->teardown(suite->userdata); +#ifndef AMIGA + } +#endif + if (testcase->success) report_success(tcnum, testcase); + else report_fail(tcnum, testcase); + testcase = testcase->next; + tcnum++; + } + } + return tcnum; +} + +/* + * Standard Runner + */ +static void report_num_tests_silent(int num_tests) { } +static void report_success_silent(int testnum, chibi_testcase *testcase) { } +static void report_fail_silent(int testnum, chibi_testcase *testcase) { } +static void report_success_std(int testnum, chibi_testcase *testcase) { fprintf(stderr, "."); } +static void report_fail_std(int testnum, chibi_testcase *testcase) { fprintf(stderr, "F"); } + +void chibi_suite_run(chibi_suite *suite, chibi_summary_data *summary) +{ + _chibi_suite_run(suite, report_num_tests_silent, report_success_std, report_fail_std, 0, 0); + if (summary) _chibi_suite_summary_data(suite, summary, 0); + chibi_suite_print_summary(suite); +} + +void chibi_suite_run_silently(chibi_suite *suite, chibi_summary_data *summary) +{ + _chibi_suite_run(suite, report_num_tests_silent, report_success_silent, report_fail_silent, 0, 0); + if (summary) _chibi_suite_summary_data(suite, summary, 0); +} + +/* + * TAP Runner + */ +static void report_num_tests_tap(int num_tests) { fprintf(stdout, "1..%d\n", num_tests); } +static void report_success_tap(int testnum, chibi_testcase *testcase) +{ + fprintf(stdout, "ok %d - %s\n", testnum + 1, testcase->fname); +} +static void report_fail_tap(int testnum, chibi_testcase *testcase) +{ + fprintf(stdout, "not ok %d - %s\n", testnum + 1, testcase->fname); +} + +void chibi_suite_run_tap(chibi_suite *suite, chibi_summary_data *summary) +{ + _chibi_suite_run(suite, report_num_tests_tap, report_success_tap, report_fail_tap, 0, 0); + if (summary) _chibi_suite_summary_data(suite, summary, 0); +} diff --git a/requesters/chibi.h b/requesters/chibi.h new file mode 100644 index 0000000..5ccfeff --- /dev/null +++ b/requesters/chibi.h @@ -0,0 +1,85 @@ +#pragma once +#ifndef __CHIBI_H__ +#define __CHIBI_H__ +#include + +#define CHIBI_TEST_GIT_SHA "$Id: 72c6f3db3710d13bf1995a89992041e29ab2f43d $" + +/* DATA STRUCTURES */ +typedef struct _chibi_testcase { + void (*fun)(struct _chibi_testcase *); + const char *fname; + struct _chibi_testcase *next; + int success; + char *error_msg; + void *userdata; + jmp_buf env; +} chibi_testcase; + +typedef void (*chibi_testfunc)(chibi_testcase *); +typedef void (*chibi_fixfunc)(void *); + +typedef struct _chibi_suite { + chibi_testcase *head; + chibi_fixfunc setup, teardown; + void *userdata; + + /* defines the head of the child list */ + struct _chibi_suite *first_child; + + /* next member in the child list */ + struct _chibi_suite *next; +} chibi_suite; + +typedef struct _chibi_summary_data { + int num_runs; + int num_pass; + int num_failures; +} chibi_summary_data; + +/* SUITE MANAGEMENT */ +extern chibi_suite *chibi_suite_new(); +extern chibi_suite *chibi_suite_new_fixture(chibi_fixfunc setup, chibi_fixfunc teardown, void *userdata); +extern void chibi_suite_delete(chibi_suite *suite); +extern void chibi_suite_run(chibi_suite *suite, chibi_summary_data *summary); +extern void chibi_suite_run_silently(chibi_suite *suite, chibi_summary_data *summary); +extern void chibi_suite_run_tap(chibi_suite *suite, chibi_summary_data *summary); + +/* + * We can nest suites. Since every suite can define a fixture, we might define + * a number of fixtures and run them as part of a larger suite. + */ +extern void chibi_suite_add_suite(chibi_suite *suite, chibi_suite *toadd); + +/* don't use this directly */ +extern void _chibi_suite_add_test(chibi_suite *suite, chibi_testfunc fun, const char *fname); + +/* + * ASSERTIONS + * don't use these directly, use the macros instead, they are more convenient + * to use. + */ +extern void _chibi_assert_not_null(chibi_testcase *tc, void *ptr, const char *msg, + const char *srcfile, int line); +extern void _chibi_fail(chibi_testcase *tc, const char *msg, const char *srcfile, int line); +extern void _chibi_assert(chibi_testcase *tc, int cond, const char *cond_str, const char *msg, + const char *srcfile, int line); +extern void _chibi_assert_eq_int(chibi_testcase *tc, int expected, int value, + const char *srcfile, int line); +extern void _chibi_assert_eq_cstr(chibi_testcase *tc, const char *expected, const char *value, + const char *srcfile, int line); + +/* MACROS */ +#define chibi_suite_add_test(suite, testfun) (_chibi_suite_add_test(suite, testfun, #testfun)) +#define CHIBI_TEST(funname) void funname(chibi_testcase *_tc) + +#define chibi_assert_not_null(arg) (_chibi_assert_not_null(_tc, arg, "argument was NULL", __FILE__, __LINE__)) +#define chibi_fail(msg) (_chibi_fail(_tc, msg, __FILE__, __LINE__)) +#define chibi_assert(cond) (_chibi_assert(_tc, cond, #cond, "condition was wrong:", __FILE__, __LINE__)) +#define chibi_assert_msg(cond, msg) (_chibi_assert(_tc, cond, "", msg, __FILE__, __LINE__)) +#define chibi_assert_eq_int(expected, value) (_chibi_assert_eq_int(_tc, expected, value, __FILE__, __LINE__)) +#define chibi_assert_eq_cstr(expected, value) (_chibi_assert_eq_cstr(_tc, expected, value, __FILE__, __LINE__)) + +#define TC_USERDATA (_tc->userdata) + +#endif /* __CHIBI_H__ */ diff --git a/requesters/dos13.c b/requesters/dos13.c index 29690ed..d9339d1 100644 --- a/requesters/dos13.c +++ b/requesters/dos13.c @@ -111,13 +111,3 @@ struct FileListEntry *scan_dir(const char *dirpath, int *num_entries) if (!dirpath) return all_volumes(num_entries); return dir_contents(dirpath, num_entries); } - -void free_file_list(struct FileListEntry *entries) -{ - struct FileListEntry *cur = entries, *next; - while (cur) { - next = cur->next; - free(cur); - cur = next; - } -} diff --git a/requesters/dos13.h b/requesters/dos13.h index 67d562a..c3a49a9 100644 --- a/requesters/dos13.h +++ b/requesters/dos13.h @@ -3,27 +3,7 @@ #ifndef __DOS13_H__ #define __DOS13_H__ -#include - -#define FILETYPE_FILE 0 -#define FILETYPE_DIR 1 -#define FILETYPE_VOLUME 2 - -// This file requester is only for 1.x, so 31 characters is the -// maximum -#define MAX_FILENAME_LEN 31 - -/* - * Store file list entries in these entries. Storing a previous pointer allows us to - * navigate backwards, e.g. for scrolling up a file list. - */ -struct FileListEntry { - struct FileListEntry *next, *prev; - UWORD file_type; - UWORD index; // index in the list - UWORD selected; - char name[MAX_FILENAME_LEN + 1]; -}; +#include "file_list.h" /* * Scans the specified directory and returns the entries, if dirpath is NULL, diff --git a/requesters/file_list.c b/requesters/file_list.c new file mode 100644 index 0000000..796f1f8 --- /dev/null +++ b/requesters/file_list.c @@ -0,0 +1,59 @@ +#include "file_list.h" +#include + +void free_file_list(struct FileListEntry *entries) +{ + struct FileListEntry *cur = entries, *next; + while (cur) { + next = cur->next; + free(cur); + cur = next; + } +} + +static struct FileListEntry *splice(struct FileListEntry *list) +{ + struct FileListEntry *fast = list, *slow = list; + while (fast->next && fast->next->next) { + fast = fast->next->next; + slow = slow->next; + } + struct FileListEntry *temp = slow->next; + slow->next = NULL; + return temp; +} + +static struct FileListEntry *merge(struct FileListEntry *list1, struct FileListEntry *list2, + BOOL asc) +{ + if (!list1) return list2; + if (!list2) return list1; + int val = strncmp(list1->name, list2->name, MAX_FILENAME_LEN); + val = asc ? val : -val; // reverse the comparison value if direction reversed + + if (val < 0) { + list1->next = merge(list1->next, list2, asc); + list1->next->prev = list1; + list1->prev = NULL; + return list1; + } else { + list2->next = merge(list1, list2->next, asc); + list2->next->prev = list2; + list2->prev = NULL; + return list2; + } +} + +struct FileListEntry *sort_file_list(struct FileListEntry *list, BOOL asc) +{ + if (!list || !list->next) return list; + struct FileListEntry *list2 = splice(list); + list = sort_file_list(list, asc); + list2 = sort_file_list(list2, asc); + return merge(list, list2, asc); +} + +struct FileListEntry *new_file_list_entry() +{ + return calloc(1, sizeof(struct FileListEntry)); +} diff --git a/requesters/file_list.h b/requesters/file_list.h new file mode 100644 index 0000000..b263546 --- /dev/null +++ b/requesters/file_list.h @@ -0,0 +1,54 @@ +#pragma once +#ifndef __FILE_LIST_H__ +#define __FILE_LIST_H__ + +/* + * File list implementation, this is a system independent module that can be unit tested + * easily. + */ +#ifdef __VBCC__ +#include + +#else + +#include +typedef uint16_t UWORD; +typedef uint8_t BOOL; +#ifndef NULL +#define NULL (0L) +#endif +#ifndef TRUE +#define TRUE (1) +#endif +#ifndef FALSE +#define FALSE (0) +#endif + +#endif /* __VBCC__ */ + +#define FILETYPE_FILE 0 +#define FILETYPE_DIR 1 +#define FILETYPE_VOLUME 2 + +// This file requester is only for 1.x, so 31 characters is the +// maximum +#define MAX_FILENAME_LEN 31 + +/* + * Store file list entries in these entries. Storing a previous pointer allows us to + * navigate backwards, e.g. for scrolling up a file list. + */ +struct FileListEntry { + struct FileListEntry *next, *prev; + UWORD file_type; + UWORD index; // index in the list + UWORD selected; + char name[MAX_FILENAME_LEN + 1]; +}; + +extern void free_file_list(struct FileListEntry *entries); + +extern struct FileListEntry *new_file_list_entry(); +extern struct FileListEntry *sort_file_list(struct FileListEntry *list, BOOL asc); + +#endif /* __FILE_LIST_H__ */ diff --git a/requesters/file_list_test.c b/requesters/file_list_test.c new file mode 100644 index 0000000..9536ba6 --- /dev/null +++ b/requesters/file_list_test.c @@ -0,0 +1,69 @@ +#include + +#include "chibi.h" +#include "file_list.h" + +CHIBI_TEST(TestMakeEntry) +{ + struct FileListEntry *entry = new_file_list_entry(); + chibi_assert_not_null(entry); + chibi_assert(entry->next == NULL); + chibi_assert(entry->prev == NULL); + free_file_list(entry); +} + +CHIBI_TEST(TestSortListNull) +{ + chibi_assert(sort_file_list(NULL, TRUE) == NULL); + chibi_assert(sort_file_list(NULL, FALSE) == NULL); +} + +CHIBI_TEST(TestSortListSingle) +{ + struct FileListEntry *entry = new_file_list_entry(); + chibi_assert(sort_file_list(entry, TRUE) == entry); + chibi_assert(sort_file_list(entry, FALSE) == entry); + free_file_list(entry); +} + +CHIBI_TEST(TestSortListTwoElemsAsc) +{ + struct FileListEntry *entry1 = new_file_list_entry(); + struct FileListEntry *entry2 = new_file_list_entry(); + strcpy(entry2->name, "abc"); + strcpy(entry1->name, "bcd"); + entry1->next = entry2; + entry2->prev = entry1; + + chibi_assert(sort_file_list(entry1, TRUE) == entry2); + free_file_list(entry2); +} + +CHIBI_TEST(TestSortListTwoElemsDesc) +{ + struct FileListEntry *entry1 = new_file_list_entry(); + struct FileListEntry *entry2 = new_file_list_entry(); + strcpy(entry2->name, "abc"); + strcpy(entry1->name, "bcd"); + entry1->next = entry2; + entry2->prev = entry1; + + chibi_assert(sort_file_list(entry1, FALSE) == entry1); + free_file_list(entry1); +} + +int main(int argc, char **argv) +{ + chibi_summary_data summary; + chibi_suite *suite = chibi_suite_new(); + chibi_suite_add_test(suite, TestMakeEntry); + chibi_suite_add_test(suite, TestSortListNull); + chibi_suite_add_test(suite, TestSortListSingle); + chibi_suite_add_test(suite, TestSortListTwoElemsAsc); + chibi_suite_add_test(suite, TestSortListTwoElemsDesc); + + chibi_suite_run(suite, &summary); + + chibi_suite_delete(suite); + return summary.num_failures; +}