diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e331a21..d6c7d3f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,6 +29,9 @@ add_library(apfl add_executable(apfl-bin main.c) target_link_libraries(apfl-bin PUBLIC apfl) +add_executable(functional-test-runner functional-test-runner.c) +target_link_libraries(functional-test-runner PUBLIC apfl) + set_target_properties(apfl-bin PROPERTIES RUNTIME_OUTPUT_NAME apfl ) @@ -43,6 +46,13 @@ unittest(tokenizer_test "") unittest(parser_test "") unittest(resizable_test "resizable.h") unittest(hashmap_test "hashmap.h") +unittest(strings_test "") + +function(functionaltest name) + add_test(NAME "functionaltest_${name}" COMMAND functional-test-runner ${CMAKE_SOURCE_DIR}/src/functional-tests/${name}.at) +endfunction() + +functionaltest("hello-world") install(TARGETS apfl DESTINATION lib) install(TARGETS apfl-bin DESTINATION bin) diff --git a/src/apfl.h b/src/apfl.h index 4e359c9..71e7d1f 100644 --- a/src/apfl.h +++ b/src/apfl.h @@ -110,10 +110,21 @@ void apfl_string_builder_init(struct apfl_allocator allocator, struct apfl_strin void apfl_string_builder_deinit(struct apfl_string_builder *); bool apfl_string_builder_append(struct apfl_string_builder *, struct apfl_string_view); bool apfl_string_builder_append_byte(struct apfl_string_builder *, char byte); +bool apfl_string_builder_append_bytes(struct apfl_string_builder *, const char *bytes, size_t len); struct apfl_string apfl_string_builder_move_string(struct apfl_string_builder *); #define apfl_string_builder_append_cstr(builder, cstr) (apfl_string_builder_append((builder), apfl_string_view_from_cstr((cstr)))) +struct apfl_string_view apfl_string_view_offset(struct apfl_string_view sv, size_t off); +struct apfl_string_view apfl_string_view_trunc(struct apfl_string_view sv, size_t newlen); +struct apfl_string_view apfl_string_view_substr(struct apfl_string_view sv, size_t off, size_t newlen); + +ptrdiff_t apfl_string_view_search(struct apfl_string_view haystack, struct apfl_string_view needle); + +struct apfl_string_view apfl_string_view_ltrim(struct apfl_string_view sv); +struct apfl_string_view apfl_string_view_rtrim(struct apfl_string_view sv); +struct apfl_string_view apfl_string_view_trim(struct apfl_string_view sv); + struct apfl_format_writer { bool (*write)(void *, const char *buf, size_t len); void *opaque; diff --git a/src/functional-test-runner.c b/src/functional-test-runner.c new file mode 100644 index 0000000..a7caa21 --- /dev/null +++ b/src/functional-test-runner.c @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include +#include + +#include "apfl.h" + +#include "alloc.h" + +enum testresult { + T_OK, + T_ERR, + T_FATAL, +}; + +struct alloc_context { + struct apfl_allocator wrapped; + jmp_buf *jmp; +}; + +struct test_parts { + struct apfl_string_view script; + struct apfl_string_view output; +}; + +APFL_NORETURN static void +fatal(jmp_buf *jmp, const char *fmt, ...) +{ + va_list varargs; + va_start(varargs, fmt); + vfprintf(stderr, fmt, varargs); + va_end(varargs); + + longjmp(*jmp, 1); +} + +static void * +alloc_cb(void *opaque, void *oldptr, size_t oldsize, size_t newsize) +{ + struct alloc_context *alloc_ctx = opaque; + + void *out = ALLOCATOR_CALL(alloc_ctx->wrapped, oldptr, oldsize, newsize); + + if (newsize != 0 && out == NULL) { + fatal( + alloc_ctx->jmp, + "Failed to allocate: oldptr=%p, oldsize=%lld, newsize=%lld\n", + oldptr, + (long long int)oldsize, + (long long int)newsize + ); + } + + return out; +} + +#define BUFSIZE 4096 + +static bool +file_get_contents(const char *filename, struct apfl_string *str, struct apfl_allocator allocator) +{ + FILE *f = fopen(filename, "r"); + if (f == NULL) { + return false; + } + + struct apfl_string_builder sb; + apfl_string_builder_init(allocator, &sb); + + char buf[BUFSIZE]; + while (!feof(f)) { + size_t len = fread(buf, 1, BUFSIZE, f); + if (!apfl_string_builder_append_bytes(&sb, buf, len)) { + apfl_string_builder_deinit(&sb); + fclose(f); + return false; + } + } + + if (!feof(f)) { + apfl_string_builder_deinit(&sb); + fclose(f); + return false; + } + + fclose(f); + + *str = apfl_string_builder_move_string(&sb); + apfl_string_builder_deinit(&sb); + + return true; +} + +bool +parse_test(const struct apfl_string str, struct test_parts *parts) +{ + struct apfl_string_view sv = apfl_string_view_from(str); + + struct apfl_string_view marker_script = apfl_string_view_from("===== script =====\n"); + struct apfl_string_view marker_output = apfl_string_view_from("===== output =====\n"); + + ptrdiff_t script = apfl_string_view_search(sv, marker_script); + ptrdiff_t output = apfl_string_view_search(sv, marker_output); + + if (script < 0 || output < 0 || script > output) { + return false; + } + + parts->script = apfl_string_view_substr( + sv, + script + marker_script.len, + output - script - marker_script.len + ); + parts->output = apfl_string_view_offset(sv, output + marker_output.len); + + return true; +} + +static void +dump_stack_error(apfl_ctx ctx, apfl_iterative_runner runner) +{ + if (apfl_iterative_runner_has_error_on_stack(runner)) { + assert(apfl_debug_print_val(ctx, -1, apfl_format_file_writer(stderr))); + } +} + +enum testresult +runtest(const char *filename) +{ + jmp_buf jmp; + + struct alloc_context alloc_ctx = { + .wrapped = apfl_allocator_default(), + .jmp = &jmp, + }; + + struct apfl_allocator allocator = { + .opaque = &alloc_ctx, + .alloc = alloc_cb, + }; + + if (setjmp(jmp) != 0) { + return T_FATAL; + } + + struct apfl_string content; + if (!file_get_contents(filename, &content, allocator)) { + fprintf(stderr, "Could not read file \"%s\"\n", filename); + return T_ERR; + } + + struct test_parts parts; + if (!parse_test(content, &parts)) { + fprintf(stderr, "Could not parse test \"%s\"\n", filename); + return T_ERR; + } + + struct apfl_string_builder output; + apfl_string_builder_init(allocator, &output); + + apfl_ctx ctx = apfl_ctx_new((struct apfl_config) { + .allocator = allocator, + .output_writer = apfl_format_string_writer(&output), + }); + + struct apfl_string_source_reader_data src_data = apfl_string_source_reader_create(parts.script); + apfl_iterative_runner runner = apfl_iterative_runner_new(ctx, apfl_string_source_reader(&src_data)); + assert(runner != NULL); + + while (apfl_iterative_runner_next(runner)) { + switch (apfl_iterative_runner_get_result(runner)) { + case APFL_RESULT_OK : + break; + case APFL_RESULT_ERR: + fprintf(stderr, "Error occurred during evaluation.\n"); + dump_stack_error(ctx, runner); + return T_ERR; + case APFL_RESULT_ERR_FATAL: + fprintf(stderr, "Fatal error occurred during evaluation.\n"); + dump_stack_error(ctx, runner); + return T_FATAL; + } + } + + apfl_iterative_runner_destroy(runner); + apfl_ctx_destroy(ctx); + + struct apfl_string output_string = apfl_string_builder_move_string(&output); + struct apfl_string_view have = apfl_string_view_trim(apfl_string_view_from(output_string)); + struct apfl_string_view want = apfl_string_view_trim(parts.output); + + if (!apfl_string_eq(have, want)) { + fprintf( + stderr, + + "Test failed\n" + "=== WANT ===\n" + APFL_STR_FMT "\n" + "=== HAVE ===\n" + APFL_STR_FMT "\n", + + APFL_STR_FMT_ARGS(want), + APFL_STR_FMT_ARGS(have) + ); + + apfl_string_deinit(allocator, &output_string); + apfl_string_deinit(allocator, &content); + + return T_ERR; + } + + apfl_string_deinit(allocator, &output_string); + apfl_string_deinit(allocator, &content); + + return T_OK; +} + +int +main(int argc, const char **argv) +{ + if (argc < 2) { + fprintf(stderr, "%s: Need at least one argument\n", argv[0]); + return 1; + } + + bool ok = true; + + for (int i = 1; i < argc; i++) { + switch (runtest(argv[i])) { + case T_OK: + break; + case T_ERR: + ok = false; + break; + case T_FATAL: + return 1; + } + } + + return ok ? 0 : 1; +} diff --git a/src/functional-tests/hello-world.at b/src/functional-tests/hello-world.at new file mode 100644 index 0000000..7100dd4 --- /dev/null +++ b/src/functional-tests/hello-world.at @@ -0,0 +1,5 @@ +===== script ===== +print "Hello World!" + +===== output ===== +Hello World! diff --git a/src/strings.c b/src/strings.c index 4131174..36a6c41 100644 --- a/src/strings.c +++ b/src/strings.c @@ -1,5 +1,7 @@ +#include #include #include +#include #include #include @@ -117,8 +119,8 @@ apfl_string_builder_deinit(struct apfl_string_builder *builder) builder_reset(builder); } -static bool -append_bytes(struct apfl_string_builder *builder, const char *bytes, size_t len) +bool +apfl_string_builder_append_bytes(struct apfl_string_builder *builder, const char *bytes, size_t len) { return apfl_resizable_append( builder->allocator, @@ -132,13 +134,13 @@ append_bytes(struct apfl_string_builder *builder, const char *bytes, size_t len) bool apfl_string_builder_append(struct apfl_string_builder *builder, struct apfl_string_view view) { - return append_bytes(builder, view.bytes, view.len); + return apfl_string_builder_append_bytes(builder, view.bytes, view.len); } bool apfl_string_builder_append_byte(struct apfl_string_builder *builder, char byte) { - return append_bytes(builder, &byte, 1); + return apfl_string_builder_append_bytes(builder, &byte, 1); } struct apfl_string @@ -166,3 +168,88 @@ apfl_string_move_into_new_gc_string(struct gc *gc, struct apfl_string *in) *str = apfl_string_move(in); return str; } + +struct apfl_string_view +apfl_string_view_offset(struct apfl_string_view sv, size_t off) +{ + if (off >= sv.len) { + return (struct apfl_string_view) { + .bytes = NULL, + .len = 0, + }; + } + + sv.bytes += off; + sv.len -= off; + + return sv; +} + +struct apfl_string_view +apfl_string_view_trunc(struct apfl_string_view sv, size_t newlen) +{ + if (newlen < sv.len) { + sv.len = newlen; + } + return sv; +} + +struct apfl_string_view +apfl_string_view_substr(struct apfl_string_view sv, size_t off, size_t newlen) +{ + sv = apfl_string_view_offset(sv, off); + sv = apfl_string_view_trunc(sv, newlen); + return sv; +} + +ptrdiff_t +apfl_string_view_search(const struct apfl_string_view haystack, const struct apfl_string_view needle) +{ + // TODO: This is not very efficient. Use a faster algorithm here. + + if (needle.len == 0) { + return 0; + } + + if (needle.len > haystack.len || haystack.len == 0) { + return -1; + } + + size_t lim = haystack.len - needle.len; + if (lim > PTRDIFF_MAX) { + return -1; + } + + for (size_t i = 0; i <= lim; i++) { + if (memcmp(haystack.bytes + i, needle.bytes, needle.len) == 0) { + return (ptrdiff_t)i; + } + } + + return -1; +} + +struct apfl_string_view +apfl_string_view_ltrim(struct apfl_string_view sv) +{ + while (sv.len > 0 && isspace(sv.bytes[0])) { + sv.len--; + sv.bytes++; + } + return sv; +} + +struct apfl_string_view +apfl_string_view_rtrim(struct apfl_string_view sv) +{ + while (sv.len > 0 && isspace(sv.bytes[sv.len - 1])) { + sv.len--; + } + return sv; +} + +struct apfl_string_view +apfl_string_view_trim(struct apfl_string_view sv) +{ + return apfl_string_view_ltrim(apfl_string_view_rtrim(sv)); +} diff --git a/src/strings_test.c b/src/strings_test.c new file mode 100644 index 0000000..4fd5115 --- /dev/null +++ b/src/strings_test.c @@ -0,0 +1,164 @@ +#include "apfl.h" +#include "test.h" + +static void +search_testcase(testctx t, const char *haystack, const char *needle, ptrdiff_t want) +{ + ptrdiff_t have = apfl_string_view_search( + apfl_string_view_from(haystack), + apfl_string_view_from(needle) + ); + + if (have != want) { + test_failf(t, "search \"%s\" in \"%s\" failed: want=%d, have=%d", needle, haystack, (int)want, (int)have); + } +} + +TEST(search, t) { + search_testcase(t, "", "", 0); + search_testcase(t, "", "foo", -1); + search_testcase(t, "foo", "foo", 0); + search_testcase(t, "foofoo", "foo", 0); + search_testcase(t, "hoofoo", "foo", 3); + search_testcase(t, "hoofooo", "foo", 3); + search_testcase(t, "ababab", "a", 0); + search_testcase(t, "ababab", "b", 1); + search_testcase(t, "ababab", "ab", 0); + search_testcase(t, "ababab", "ababab", 0); +} + +typedef struct apfl_string_view (*trimmer)(struct apfl_string_view); + +static void +generic_trim_testcase(testctx t, const char *name, trimmer trim, const char *in, const char *want) +{ + struct apfl_string_view have = trim(apfl_string_view_from(in)); + if (!apfl_string_eq(want, have)) { + test_failf(t, "%s of \"%s\" failed: want=\"%s\", have=\"" APFL_STR_FMT "\"", name, in, want, APFL_STR_FMT_ARGS(have)); + } +} + +static void +ltrim_testcase(testctx t, const char *in, const char *want) +{ + generic_trim_testcase(t, "ltrim", apfl_string_view_ltrim, in, want); +} + +static void +rtrim_testcase(testctx t, const char *in, const char *want) +{ + generic_trim_testcase(t, "rtrim", apfl_string_view_rtrim, in, want); +} + +static void +trim_testcase(testctx t, const char *in, const char *want) +{ + generic_trim_testcase(t, "trim", apfl_string_view_trim, in, want); +} + +TEST(ltrim, t) { + ltrim_testcase(t, "", ""); + ltrim_testcase(t, " ", ""); + ltrim_testcase(t, " ", ""); + ltrim_testcase(t, " \n", ""); + ltrim_testcase(t, " \t\n", ""); + ltrim_testcase(t, " foo", "foo"); + ltrim_testcase(t, " foo ", "foo "); + ltrim_testcase(t, "foo ", "foo "); + ltrim_testcase(t, "foo\t", "foo\t"); + ltrim_testcase(t, " f o o ", "f o o "); +} + +TEST(rtrim, t) { + rtrim_testcase(t, "", ""); + rtrim_testcase(t, " ", ""); + rtrim_testcase(t, " ", ""); + rtrim_testcase(t, " \n", ""); + rtrim_testcase(t, " \t\n", ""); + rtrim_testcase(t, " foo", " foo"); + rtrim_testcase(t, " foo ", " foo"); + rtrim_testcase(t, "foo ", "foo"); + rtrim_testcase(t, "foo\t", "foo"); + rtrim_testcase(t, " f o o ", " f o o"); +} + +TEST(trim, t) { + trim_testcase(t, "", ""); + trim_testcase(t, " ", ""); + trim_testcase(t, " ", ""); + trim_testcase(t, " \n", ""); + trim_testcase(t, " \t\n", ""); + trim_testcase(t, " foo", "foo"); + trim_testcase(t, " foo ", "foo"); + trim_testcase(t, "foo ", "foo"); + trim_testcase(t, "foo\t", "foo"); + trim_testcase(t, " f o o ", "f o o"); +} + +static void +offset_testcase(testctx t, const char *in, size_t off, const char *want) +{ + struct apfl_string_view have = apfl_string_view_offset(apfl_string_view_from(in), off); + if (!apfl_string_eq(want, have)) { + test_failf(t, "offset(\"%s\", %d) failed: want=\"%s\", have=\"" APFL_STR_FMT "\"", in, (int)off, want, APFL_STR_FMT_ARGS(have)); + } +} + +TEST(offset, t) { + offset_testcase(t, "", 0, ""); + offset_testcase(t, "", 10, ""); + offset_testcase(t, "foobar", 0, "foobar"); + offset_testcase(t, "foobar", 1, "oobar"); + offset_testcase(t, "foobar", 3, "bar"); + offset_testcase(t, "foobar", 6, ""); + offset_testcase(t, "foobar", 1000, ""); +} + +static void +trunc_testcase(testctx t, const char *in, size_t newlen, const char *want) +{ + struct apfl_string_view have = apfl_string_view_trunc(apfl_string_view_from(in), newlen); + if (!apfl_string_eq(want, have)) { + test_failf(t, "trunc(\"%s\", %d) failed: want=\"%s\", have=\"" APFL_STR_FMT "\"", in, (int)newlen, want, APFL_STR_FMT_ARGS(have)); + } +} + +TEST(trunc, t) { + trunc_testcase(t, "", 0, ""); + trunc_testcase(t, "", 10, ""); + trunc_testcase(t, "foobar", 6, "foobar"); + trunc_testcase(t, "foobar", 10000, "foobar"); + trunc_testcase(t, "foobar", 0, ""); + trunc_testcase(t, "foobar", 5, "fooba"); + trunc_testcase(t, "foobar", 3, "foo"); +} + +static void +substr_testcase(testctx t, const char *in, size_t off, size_t newlen, const char *want) +{ + struct apfl_string_view have = apfl_string_view_substr(apfl_string_view_from(in), off, newlen); + if (!apfl_string_eq(want, have)) { + test_failf(t, "substr(\"%s\", %d, %d) failed: want=\"%s\", have=\"" APFL_STR_FMT "\"", in, (int)off, (int)newlen, want, APFL_STR_FMT_ARGS(have)); + } +} + +TEST(substr, t) { + substr_testcase(t, "", 0, 0, ""); + substr_testcase(t, "", 10, 0, ""); + substr_testcase(t, "", 0, 10, ""); + substr_testcase(t, "foobar", 0, 10, "foobar"); + substr_testcase(t, "foobar", 0, 6, "foobar"); + substr_testcase(t, "foobar", 0, 5, "fooba"); + substr_testcase(t, "foobar", 1, 5, "oobar"); + substr_testcase(t, "foobar", 100, 5, ""); +} + +TESTS_BEGIN + ADDTEST(search), + ADDTEST(ltrim), + ADDTEST(rtrim), + ADDTEST(trim), + ADDTEST(offset), + ADDTEST(trunc), + ADDTEST(substr), +TESTS_END