/** * @file hoststruct.c * @brief Tests sharing a host C struct with a script. * * The assertions that matter are the ones about *conversion*, because that is * the boundary where the two type systems disagree and where a silent answer * would be worst. A range check that quietly wraps is found three frames later * in code that did nothing wrong, so every refusal is asserted individually. * * The sharing itself is asserted in both directions: what the script sees when * the host changes its struct underneath, and what the host sees when the script * writes. */ #include #include #include #include #include #include #include #include "harness.h" #include "testutil.h" typedef struct { char name[8]; int8_t tiny; int16_t small; int32_t hp; uint8_t level; float x; double weight; bool hostile; } test_Enemy; static const akbasic_HostField ENEMY_FIELDS[] = { /* struct member BASIC name C representation */ AKBASIC_HOST_FIELD( test_Enemy, name, "NAME$", AKBASIC_HOSTFIELD_CSTRING ), AKBASIC_HOST_FIELD( test_Enemy, tiny, "TINY#", AKBASIC_HOSTFIELD_INT8 ), AKBASIC_HOST_FIELD( test_Enemy, small, "SMALL#", AKBASIC_HOSTFIELD_INT16 ), AKBASIC_HOST_FIELD( test_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ), AKBASIC_HOST_FIELD( test_Enemy, level, "LEVEL#", AKBASIC_HOSTFIELD_UINT8 ), AKBASIC_HOST_FIELD( test_Enemy, x, "X%", AKBASIC_HOSTFIELD_FLOAT ), AKBASIC_HOST_FIELD( test_Enemy, weight, "WEIGHT%", AKBASIC_HOSTFIELD_DOUBLE ), AKBASIC_HOST_FIELD( test_Enemy, hostile, "HOSTILE#", AKBASIC_HOSTFIELD_BOOL ) }; static const akbasic_HostType ENEMY_TYPE = { "ENEMY", sizeof(test_Enemy), ENEMY_FIELDS, 8 }; static test_Enemy ENEMY; /** @brief A fresh runtime with ENEMY registered and bound to FOE@. */ static akerr_ErrorContext AKERR_NOIGNORE *bind_and_run(const char *program) { PREPARE_ERROR(errctx); memset(&ENEMY, 0, sizeof(ENEMY)); snprintf(ENEMY.name, sizeof(ENEMY.name), "GOBLIN"); ENEMY.tiny = 1; ENEMY.small = 2; ENEMY.hp = 30; ENEMY.level = 3; ENEMY.x = 1.5f; ENEMY.weight = 2.25; ENEMY.hostile = true; PASS(errctx, harness_start(NULL)); PASS(errctx, akbasic_host_register_type(&HARNESS_RUNTIME, &ENEMY_TYPE)); PASS(errctx, akbasic_host_bind(&HARNESS_RUNTIME, "FOE@", "ENEMY", &ENEMY)); PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, program)); PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 4000)); SUCCEED_RETURN(errctx); } /** @brief Every C representation reaches the script as the right BASIC value. */ static void test_reads(void) { TEST_REQUIRE_OK(bind_and_run("10 PRINT FOE@.NAME$\n" "20 PRINT FOE@.TINY#\n" "30 PRINT FOE@.SMALL#\n" "40 PRINT FOE@.HP#\n" "50 PRINT FOE@.LEVEL#\n" "60 PRINT FOE@.WEIGHT%\n" "70 PRINT FOE@.HOSTILE#\n")); /* A bool reads as -1, the Commodore convention every condition already uses. */ TEST_REQUIRE_STR(HARNESS_OUTPUT, "GOBLIN\n1\n2\n30\n3\n2.250000\n-1\n"); harness_stop(); } /** @brief A write reaches the host's own struct, with no step in between. */ static void test_writes_reach_the_host(void) { TEST_REQUIRE_OK(bind_and_run("10 FOE@.HP# = FOE@.HP# - 10\n" "20 FOE@.NAME$ = \"ORC\"\n" "30 FOE@.HOSTILE# = 0\n" "40 FOE@.WEIGHT% = 9.5\n")); TEST_REQUIRE_INT(ENEMY.hp, 20); TEST_REQUIRE_STR(ENEMY.name, "ORC"); TEST_REQUIRE(ENEMY.hostile == false, "a zero should have cleared the host's bool"); TEST_REQUIRE_FEQ(ENEMY.weight, 9.5); harness_stop(); } /** * @brief The script sees what the host holds *now*, not a snapshot. * * The half of "shared" that a copy-in binding would get wrong: the game moves * its own data between statements and the script has to see it. */ static void test_host_changes_are_visible(void) { TEST_REQUIRE_OK(bind_and_run("10 PRINT FOE@.HP#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n"); harness_stop(); /* Change it behind the script's back, then run again. */ TEST_REQUIRE_OK(harness_start(NULL)); memset(&ENEMY, 0, sizeof(ENEMY)); ENEMY.hp = 30; TEST_REQUIRE_OK(akbasic_host_register_type(&HARNESS_RUNTIME, &ENEMY_TYPE)); TEST_REQUIRE_OK(akbasic_host_bind(&HARNESS_RUNTIME, "FOE@", "ENEMY", &ENEMY)); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT FOE@.HP#\n")); ENEMY.hp = 77; TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000)); TEST_REQUIRE_STR(HARNESS_OUTPUT, "77\n"); harness_stop(); } /** * @brief A value that will not fit is refused, by field, rather than wrapped. * * One case per width, because a range check is exactly the sort of thing that is * right for `int32_t` and wrong for `int8_t` when only one of them is tested. */ static void test_conversion_refuses_rather_than_truncates(void) { static const char *CASES[] = { "10 FOE@.TINY# = 200\n", /* int8_t */ "10 FOE@.SMALL# = 70000\n", /* int16_t */ "10 FOE@.LEVEL# = -1\n", /* uint8_t */ "10 FOE@.NAME$ = \"MUCH TOO LONG\"\n" /* char[8] */ }; size_t i = 0; for ( i = 0; i < sizeof(CASES) / sizeof(CASES[0]); i++ ) { TEST_REQUIRE_OK(bind_and_run(CASES[i])); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "does not fit") != NULL, "case %zu should be refused rather than truncated, got \"%s\"", i, HARNESS_OUTPUT); harness_stop(); } /* And the host's struct is untouched by a refused write. */ TEST_REQUIRE_OK(bind_and_run("10 FOE@.TINY# = 200\n")); TEST_REQUIRE_INT(ENEMY.tiny, 1); harness_stop(); } /** * @brief Copy still copies, and POINT still shares -- across the boundary too. * * The reason there is one API rather than two: the language's own distinction * turns out to be exactly the one a host needs, so which one a script used is * visible in the listing. */ static void test_copy_versus_point(void) { TEST_REQUIRE_OK(bind_and_run("10 TYPE MINE\n" "20 HP#\n" "30 END TYPE\n" "40 DIM SNAP@ AS MINE\n" "50 SNAP@.HP# = FOE@.HP#\n" "60 SNAP@.HP# = 0\n" "70 PRINT FOE@.HP#\n")); /* The snapshot moved; the game did not. */ TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n"); TEST_REQUIRE_INT(ENEMY.hp, 30); harness_stop(); TEST_REQUIRE_OK(bind_and_run("10 DIM LIVE@ AS PTR TO ENEMY\n" "20 POINT LIVE@ AT FOE@\n" "30 LIVE@->HP# = 5\n")); TEST_REQUIRE_INT(ENEMY.hp, 5); harness_stop(); } /** @brief A suffix that disagrees with the C type is refused at registration. */ static void test_suffix_must_match_the_c_type(void) { static const akbasic_HostField WRONG[] = { AKBASIC_HOST_FIELD( test_Enemy, hp, "HP%", AKBASIC_HOSTFIELD_INT32 ) }; static const akbasic_HostType WRONG_TYPE = { "WRONGT", sizeof(test_Enemy), WRONG, 1 }; akerr_ErrorContext *raised = NULL; TEST_REQUIRE_OK(harness_start(NULL)); raised = akbasic_host_register_type(&HARNESS_RUNTIME, &WRONG_TYPE); TEST_REQUIRE(raised != NULL, "a float suffix over an int32_t must be refused"); TEST_REQUIRE_INT(raised->status, AKBASIC_ERR_VALUE); test_discard_error(raised); harness_stop(); } /** * @brief After unbinding, the name is refused rather than read. * * The whole reason the entry point exists: a binding is the only pointer this * interpreter holds that it did not allocate, so a host has to be able to take * it back before the storage goes. */ static void test_unbind_refuses_later_reads(void) { TEST_REQUIRE_OK(harness_start(NULL)); memset(&ENEMY, 0, sizeof(ENEMY)); ENEMY.hp = 30; TEST_REQUIRE_OK(akbasic_host_register_type(&HARNESS_RUNTIME, &ENEMY_TYPE)); TEST_REQUIRE_OK(akbasic_host_bind(&HARNESS_RUNTIME, "FOE@", "ENEMY", &ENEMY)); TEST_REQUIRE_OK(akbasic_host_unbind(&HARNESS_RUNTIME, "FOE@")); TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT FOE@.HP#\n")); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "ERROR") != NULL, "reading an unbound name should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief A binding survives the prescan, which runs again on every RUN. */ static void test_registration_survives_a_rerun(void) { TEST_REQUIRE_OK(bind_and_run("10 PRINT FOE@.HP#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n"); TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN)); TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 4000)); /* * The prescan clears what the *script* declared and keeps what the *host* * registered. Wiping the table outright unregistered the host's types the * first time a script ran, with no way for the host to know it had to * register them again. */ TEST_REQUIRE_STR(HARNESS_OUTPUT, "30\n30\n"); harness_stop(); } int main(void) { TEST_REQUIRE_OK(akbasic_error_register()); test_reads(); test_writes_reach_the_host(); test_host_changes_are_visible(); test_conversion_refuses_rather_than_truncates(); test_copy_versus_point(); test_suffix_must_match_the_c_type(); test_unbind_refuses_later_reads(); test_registration_survives_a_rerun(); return akbasic_test_failures; }