266 lines
9.4 KiB
C
266 lines
9.4 KiB
C
|
|
/**
|
||
|
|
* @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 <stdbool.h>
|
||
|
|
#include <stddef.h>
|
||
|
|
#include <stdint.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/host.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|