tally/engine/test/c_abi_test.c
2026-10-03 12:21:02 -07:00

169 lines
7.9 KiB
C

/* Calls the C ABI the way a JNI or Swift binding will: through include/tally.h,
* linked against the real library.
*
* The Zig tests in c_api.zig cover behaviour and are leak-checked. This covers what
* they cannot: that the header matches the library, that the symbols are findable by a
* C linker, and that the struct layouts agree across the language boundary. A wrong
* field order in tally.h is invisible from Zig and fatal in the field.
*/
#include "tally.h"
#include <stdio.h>
#include <string.h>
static int failures = 0;
static void check(int condition, const char *what) {
if (!condition) {
printf("FAIL: %s\n", what);
failures++;
}
}
static void check_contains(const uint8_t *json, size_t len, const char *needle, const char *what) {
/* The JSON is not null-terminated: it is a borrowed (ptr, len). */
char buf[4096];
size_t n = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
memcpy(buf, json, n);
buf[n] = '\0';
if (strstr(buf, needle) == NULL) {
printf("FAIL: %s\n wanted: %s\n got: %s\n", what, needle, buf);
failures++;
}
}
int main(void) {
check(tally_abi_version() == TALLY_ABI_VERSION, "header and library agree on the ABI version");
size_t version_len = 0;
const uint8_t *version = tally_version(&version_len);
check(version_len == 5 && memcmp(version, "0.1.0", 5) == 0, "tally_version");
tally_session *session = tally_session_new();
check(session != NULL, "tally_session_new");
if (session == NULL) return 1;
const uint8_t *json = NULL;
size_t len = 0;
/* A session that remembers: the whole reason for a handle. */
const char *assign = "x = 2^100";
check(tally_eval(session, (const uint8_t *)assign, strlen(assign),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK,
"assignment evaluates");
const char *use = "x + 1";
check(tally_eval(session, (const uint8_t *)use, strlen(use),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK,
"the variable survives into the next call");
check_contains(json, len, "1,267,650,600,228,229,401,496,703,205,377",
"exact 128-bit arithmetic crosses the boundary intact");
const char *ans = "Ans / 2";
check(tally_eval(session, (const uint8_t *)ans, strlen(ans),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "Ans is readable");
check_contains(json, len, "633,825,300,114,114,700,748,351,602,688.5",
"half of an odd number, so the exact tier is really exact");
/* A preview answers and stores nothing: neither the variable nor Ans. */
const char *peek = "y = 41 + 1";
check(tally_preview(session, (const uint8_t *)peek, strlen(peek),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "preview evaluates");
check_contains(json, len, "\"display\":\"42\"", "a preview carries the answer");
const char *y = "y";
check(tally_eval(session, (const uint8_t *)y, strlen(y),
TALLY_MODE_STANDARD, &json, &len) == TALLY_EVAL_ERROR,
"a previewed assignment stored nothing");
const char *same = "Ans";
check(tally_eval(session, (const uint8_t *)same, strlen(same),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "Ans after a preview");
check_contains(json, len, "633,825,300,114,114,700,748,351,602,688.5",
"Ans is still the last committed answer");
/* An error is a status plus the engine's own wording. */
const char *bad = "1 / 0";
check(tally_eval(session, (const uint8_t *)bad, strlen(bad),
TALLY_MODE_STANDARD, &json, &len) == TALLY_EVAL_ERROR,
"division by zero is an eval error");
check_contains(json, len, "division by zero", "the error carries the engine's phrase");
/* Unit conversion, from standard mode, with no separate entry point. */
const char *convert = "100 km to mi";
check(tally_eval(session, (const uint8_t *)convert, strlen(convert),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "conversion evaluates");
check_contains(json, len, "\"from\":\"km\"", "the conversion names its units");
/* The config struct crosses the boundary with its fields where the header says. */
tally_config config;
check(tally_session_config(session, &config) == TALLY_OK, "reading the config");
check(config.bits == 64, "default width is 64");
config.bits = 8;
config.is_signed = 1;
check(tally_session_configure(session, &config) == TALLY_OK, "configuring 8-bit signed");
const char *ff = "0xFF";
check(tally_eval(session, (const uint8_t *)ff, strlen(ff),
TALLY_MODE_PROGRAMMER, &json, &len) == TALLY_OK, "programmer mode");
check_contains(json, len, "\"dec_signed\":\"-1\"",
"0xFF read as 8-bit signed is -1, so bits and is_signed both landed");
check_contains(json, len, "\"bits\":8", "the width reached the result");
config.bits = 24;
check(tally_session_configure(session, &config) == TALLY_INVALID_ARGUMENT,
"a width the engine has no name for is refused");
/* The catalogue a picker is built from. */
check(tally_unit_catalog(session, &json, &len) == TALLY_OK, "unit catalogue");
check_contains(json, len, "\"base_unit\":\"m\"", "length's base unit");
check_contains(json, len, "kilometer", "aliases come along for searching");
/* check_contains reads the first 4KB; length is the first category. */
check_contains(json, len, "{\"name\":\"km\",\"label\":\"Kilometer\"", "units carry a display name");
/* One value in every unit of its category, for a converter screen. */
const char *hundred = "100";
const char *celsius = "C";
check(tally_convert(session, (const uint8_t *)hundred, strlen(hundred),
(const uint8_t *)celsius, strlen(celsius), &json, &len) == TALLY_OK,
"tally_convert");
check_contains(json, len, "{\"unit\":\"F\",\"display\":\"212\"", "100 C is 212 F");
check_contains(json, len, "{\"unit\":\"K\",\"display\":\"373.15\"", "and 373.15 K");
/* Save one session and load it into another: x and Ans come across exactly. */
const char *keep = "x = 2^100";
check(tally_eval(session, (const uint8_t *)keep, strlen(keep),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "assign before save");
check(tally_session_save(session, &json, &len) == TALLY_OK, "tally_session_save");
check_contains(json, len, "\"name\":\"x\",\"value\":{\"exact\":\"1267650600228229401496703205376\"}",
"an exact value is saved with every digit");
tally_session *restored = tally_session_new();
check(restored != NULL, "a second session");
if (restored != NULL) {
/* The saved bytes belong to the first session, so they are still valid here. */
check(tally_session_load(restored, json, len) == TALLY_OK, "tally_session_load");
const char *use_x = "x + 1";
check(tally_eval(restored, (const uint8_t *)use_x, strlen(use_x),
TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "x after load");
check_contains(json, len, "1,267,650,600,228,229,401,496,703,205,377",
"the loaded variable is the saved one");
const char *junk = "{\"format\":1}";
check(tally_session_load(restored, (const uint8_t *)junk, strlen(junk)) == TALLY_INVALID_ARGUMENT,
"a malformed state is refused");
tally_session_free(restored);
}
/* Null handling, since a cleanup path should not need its own checks. */
check(tally_eval(NULL, (const uint8_t *)use, strlen(use),
TALLY_MODE_STANDARD, &json, &len) == TALLY_INVALID_ARGUMENT,
"a null session is refused");
tally_session_free(NULL);
tally_session_free(session);
if (failures == 0) {
printf("c_abi_test: all checks passed\n");
return 0;
}
printf("c_abi_test: %d check(s) failed\n", failures);
return 1;
}