Implement a simple test runner for functional tests

This commit is contained in:
Laria 2022-10-30 22:00:11 +01:00
parent 145fcddbed
commit b7015e7b13
6 changed files with 524 additions and 4 deletions

View file

@ -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)

View file

@ -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;

View file

@ -0,0 +1,243 @@
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <setjmp.h>
#include <stdarg.h>
#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;
}

View file

@ -0,0 +1,5 @@
===== script =====
print "Hello World!"
===== output =====
Hello World!

View file

@ -1,5 +1,7 @@
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
@ -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));
}

164
src/strings_test.c Normal file
View file

@ -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