169 lines
7.9 KiB
C
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;
|
|
}
|