Port the BASIC interpreter from Go to C

Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.

The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.

Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.

Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.

src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.

Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.

Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.

The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.

ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 23:53:56 -04:00
commit f8c15c2f2c
60 changed files with 10192 additions and 0 deletions

56
tests/convert.c Normal file
View File

@@ -0,0 +1,56 @@
/**
* @file convert.c
* @brief Tests the strict string-to-number converters.
*
* These exist because libakstdlib's aksl_ato* family cannot report a conversion
* failure. Every assertion here is one that family would get wrong.
*/
#include <errno.h>
#include <akbasic/convert.h>
#include <akbasic/error.h>
#include "testutil.h"
int main(void)
{
int64_t i = 0;
double d = 0.0;
TEST_REQUIRE_OK(akbasic_error_register());
/* Valid decimal, hex and negative. */
TEST_REQUIRE_OK(akbasic_str_to_int64("1234", 10, &i));
TEST_REQUIRE_INT(i, 1234);
TEST_REQUIRE_OK(akbasic_str_to_int64("0xff", 16, &i));
TEST_REQUIRE_INT(i, 255);
TEST_REQUIRE_OK(akbasic_str_to_int64("-256", 10, &i));
TEST_REQUIRE_INT(i, -256);
/*
* The whole point: aksl_atoi returns success with 0 for each of these.
*/
TEST_REQUIRE_STATUS(akbasic_str_to_int64("", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("not a number", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("12abc", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("12 ", 10, &i), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("99999999999999999999999", 10, &i), ERANGE);
TEST_REQUIRE_STATUS(akbasic_str_to_int64(NULL, 10, &i), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_str_to_int64("1", 10, NULL), AKERR_NULLPOINTER);
/* Floats. */
TEST_REQUIRE_OK(akbasic_str_to_double("123.456", &d));
TEST_REQUIRE_FEQ(d, 123.456);
TEST_REQUIRE_OK(akbasic_str_to_double("-2.5", &d));
TEST_REQUIRE_FEQ(d, -2.5);
TEST_REQUIRE_OK(akbasic_str_to_double("32", &d));
TEST_REQUIRE_FEQ(d, 32.0);
TEST_REQUIRE_STATUS(akbasic_str_to_double("", &d), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_double("garbage", &d), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_str_to_double("1.5x", &d), AKBASIC_ERR_VALUE);
return akbasic_test_failures;
}

100
tests/environment_scope.c Normal file
View File

@@ -0,0 +1,100 @@
/**
* @file environment_scope.c
* @brief Tests scoping, label creation, the wait mechanism and pool release.
*/
#include "harness.h"
int main(void)
{
akbasic_Environment *root = NULL;
akbasic_Environment *child = NULL;
akbasic_Variable *variable = NULL;
akbasic_Variable *again = NULL;
akbasic_Value *value = NULL;
int64_t lineno = 0;
int i = 0;
TEST_REQUIRE_OK(harness_start(NULL));
root = HARNESS_RUNTIME.environment;
/* The active environment auto-creates on a miss. */
TEST_REQUIRE_OK(akbasic_environment_get(root, "A#", &variable));
TEST_REQUIRE(variable != NULL, "the active environment must auto-create a variable");
TEST_REQUIRE_INT(variable->valuetype, AKBASIC_TYPE_INTEGER);
/* And the second lookup finds the same one. */
TEST_REQUIRE_OK(akbasic_environment_get(root, "A#", &again));
TEST_REQUIRE(again == variable, "a repeat lookup must find the same variable");
/* A child sees its parent's variables. */
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
child = HARNESS_RUNTIME.environment;
TEST_REQUIRE(child->parent == root, "the child must chain to its parent");
TEST_REQUIRE_OK(akbasic_environment_get(child, "A#", &again));
TEST_REQUIRE(again == variable, "the child must see the parent's variable");
/*
* A parent does not create variables on behalf of a child: looking up a
* missing name in a non-active environment yields NULL without error.
*/
TEST_REQUIRE_OK(akbasic_environment_get(root, "ZZ#", &again));
TEST_REQUIRE(again == NULL, "a non-active environment must not auto-create");
/* Labels are created only at the top level, and are visible from below. */
TEST_REQUIRE_OK(akbasic_environment_set_label(child, "LOOPTOP", 30));
TEST_REQUIRE_OK(akbasic_environment_get_label(child, "LOOPTOP", &lineno));
TEST_REQUIRE_INT(lineno, 30);
TEST_REQUIRE_STATUS(akbasic_environment_get_label(child, "MISSING", &lineno),
AKBASIC_ERR_UNDEFINED);
/* Popping releases the environment back to the pool. */
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
TEST_REQUIRE(HARNESS_RUNTIME.environment == root, "pop must restore the parent");
TEST_REQUIRE(!child->used, "pop must release the environment to the pool");
/* The root has no parent to pop to. */
TEST_REQUIRE_STATUS(akbasic_runtime_prev_environment(&HARNESS_RUNTIME),
AKBASIC_ERR_ENVIRONMENT);
/* The wait mechanism, including the parent-chain search. */
TEST_REQUIRE(!akbasic_environment_is_waiting_for_any(root), "a fresh environment waits for nothing");
TEST_REQUIRE_OK(akbasic_environment_wait_for_command(root, "NEXT"));
TEST_REQUIRE(akbasic_environment_is_waiting_for_any(root), "the wait must register");
TEST_REQUIRE(akbasic_environment_is_waiting_for(root, "NEXT"), "the wait must match by name");
TEST_REQUIRE(!akbasic_environment_is_waiting_for(root, "DATA"), "a different verb must not match");
/* Two pending waits in one environment is a hard error, not a panic. */
TEST_REQUIRE_STATUS(akbasic_environment_wait_for_command(root, "DATA"), AKBASIC_ERR_STATE);
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
child = HARNESS_RUNTIME.environment;
TEST_REQUIRE(akbasic_environment_is_waiting_for(child, "NEXT"),
"a child must see its parent's wait");
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
TEST_REQUIRE_OK(akbasic_environment_stop_waiting(root, "NEXT"));
TEST_REQUIRE(!akbasic_environment_is_waiting_for_any(root), "stop_waiting must clear it");
/* The per-line value pool is bounded and says so. */
for ( i = 0; i < AKBASIC_MAX_VALUES; i++ ) {
TEST_REQUIRE_OK(akbasic_environment_new_value(root, &value));
}
TEST_REQUIRE_STATUS(akbasic_environment_new_value(root, &value), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_OK(akbasic_environment_zero(root));
TEST_REQUIRE_OK(akbasic_environment_new_value(root, &value));
/* So is the environment pool. */
for ( i = 0; i < AKBASIC_MAX_ENVIRONMENTS - 1; i++ ) {
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
}
TEST_REQUIRE_STATUS(akbasic_runtime_new_environment(&HARNESS_RUNTIME),
AKBASIC_ERR_ENVIRONMENT);
for ( i = 0; i < AKBASIC_MAX_ENVIRONMENTS - 1; i++ ) {
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
}
TEST_REQUIRE(HARNESS_RUNTIME.environment == root, "the pool must unwind back to the root");
harness_stop();
return akbasic_test_failures;
}

50
tests/error_codes.c Normal file
View File

@@ -0,0 +1,50 @@
/**
* @file error_codes.c
* @brief Tests that akbasic owns its status band and has named every code in it.
*/
#include <string.h>
#include <akbasic/error.h>
#include "testutil.h"
int main(void)
{
int code = 0;
/* Registering is idempotent: an identical repeat by the same owner is a no-op. */
TEST_REQUIRE_OK(akbasic_error_register());
TEST_REQUIRE_OK(akbasic_error_register());
/*
* Every code must read back a real name. An unnamed one degrades to
* "Unknown Error" in every stack trace that carries it, which nothing else
* in the suite would notice.
*/
for ( code = AKBASIC_ERR_SYNTAX; code <= AKBASIC_ERR_STATE; code++ ) {
const char *name = akerr_name_for_status(code, NULL);
TEST_REQUIRE(name != NULL, "status %d has a NULL name", code);
TEST_REQUIRE(strcmp(name, "Unknown Error") != 0,
"status %d reads back as \"Unknown Error\"", code);
}
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_SYNTAX, NULL), "Syntax Error");
TEST_REQUIRE_STR(akerr_name_for_status(AKBASIC_ERR_STATE, NULL), "State Error");
/*
* The assertion that proves the range is actually ours rather than merely
* unclaimed: a different owner must be refused.
*/
TEST_REQUIRE_STATUS(akerr_register_status_name("not-akbasic", AKBASIC_ERR_SYNTAX, "Hijacked"),
AKERR_STATUS_NAME_FOREIGN);
TEST_REQUIRE_STATUS(akerr_reserve_status_range(AKBASIC_ERR_BASE, 4, "not-akbasic"),
AKERR_STATUS_RANGE_OVERLAP);
/* The band must sit clear of libakerror's reserved 0-255. */
TEST_REQUIRE(AKBASIC_ERR_BASE >= AKERR_FIRST_CONSUMER_STATUS,
"AKBASIC_ERR_BASE %d is inside libakerror's reserved band",
AKBASIC_ERR_BASE);
return akbasic_test_failures;
}

42
tests/golden.cmake Normal file
View File

@@ -0,0 +1,42 @@
# Golden-file driver: run one .bas through the interpreter and byte-compare
# stdout against the sibling .txt.
#
# The comparison is on raw bytes, not lines: the reference's print pipeline emits
# a trailing double newline on an error line (basicError builds a string ending
# in \n and hands it to Println, which adds another), and that is part of the
# contract. See TODO.md section 1.8.
if(NOT DEFINED BASIC OR NOT DEFINED CASE)
message(FATAL_ERROR "golden.cmake requires -DBASIC=<interpreter> -DCASE=<file.bas>")
endif()
string(REGEX REPLACE "\\.bas$" ".txt" EXPECTED "${CASE}")
if(NOT EXISTS "${EXPECTED}")
message(FATAL_ERROR "No golden output beside ${CASE}")
endif()
execute_process(
COMMAND "${BASIC}" "${CASE}"
OUTPUT_VARIABLE actual
ERROR_VARIABLE stderr_text
RESULT_VARIABLE rc
)
if(NOT rc EQUAL 0)
message(FATAL_ERROR "${CASE}: interpreter exited ${rc}\n${stderr_text}")
endif()
file(READ "${EXPECTED}" expected)
if(NOT actual STREQUAL expected)
# Write what we got beside the build so a failure can be diffed by hand.
get_filename_component(_base "${CASE}" NAME)
set(_dump "${CMAKE_CURRENT_BINARY_DIR}/${_base}.actual")
file(WRITE "${_dump}" "${actual}")
message(FATAL_ERROR
"${CASE}: output does not match ${EXPECTED}\n"
"--- expected ---\n${expected}\n"
"--- actual ---\n${actual}\n"
"(actual written to ${_dump})")
endif()

105
tests/grammar_leaves.c Normal file
View File

@@ -0,0 +1,105 @@
/**
* @file grammar_leaves.c
* @brief Tests AST leaf construction, rendering and deep cloning.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/grammar.h>
#include "testutil.h"
static akbasic_ASTLeaf LEAVES[8];
static akbasic_ASTLeaf STORAGE[AKBASIC_MAX_LEAVES];
static akbasic_LeafPool POOL;
int main(void)
{
akbasic_ASTLeaf *copy = NULL;
char rendered[AKBASIC_MAX_STRING_LENGTH];
TEST_REQUIRE_OK(akbasic_error_register());
POOL.next = 0;
POOL.capacity = AKBASIC_MAX_LEAVES;
POOL.leaves = STORAGE;
/* Literals render the way PRINT renders them. */
TEST_REQUIRE_OK(akbasic_leaf_new_literal_int(&LEAVES[0], "42"));
TEST_REQUIRE_INT(LEAVES[0].literal_int, 42);
TEST_REQUIRE_OK(akbasic_leaf_to_string(&LEAVES[0], rendered, sizeof(rendered)));
TEST_REQUIRE_STR(rendered, "42");
/* 0x is hex. */
TEST_REQUIRE_OK(akbasic_leaf_new_literal_int(&LEAVES[1], "0xff"));
TEST_REQUIRE_INT(LEAVES[1].literal_int, 255);
/*
* A leading zero selects base 8, so 010 is 8 and 08 will not parse.
* Commodore BASIC has no octal literals; this is TODO.md section 12 item 10,
* reproduced deliberately and pinned here so the eventual fix is a
* deliberate change to this assertion.
*/
TEST_REQUIRE_OK(akbasic_leaf_new_literal_int(&LEAVES[1], "010"));
TEST_REQUIRE_INT(LEAVES[1].literal_int, 8);
TEST_REQUIRE_ANY_ERROR(akbasic_leaf_new_literal_int(&LEAVES[1], "08"));
TEST_REQUIRE_OK(akbasic_leaf_new_literal_float(&LEAVES[2], "2.5"));
TEST_REQUIRE_FEQ(LEAVES[2].literal_float, 2.5);
TEST_REQUIRE_OK(akbasic_leaf_to_string(&LEAVES[2], rendered, sizeof(rendered)));
TEST_REQUIRE_STR(rendered, "2.500000");
TEST_REQUIRE_OK(akbasic_leaf_new_literal_string(&LEAVES[3], "HELLO"));
TEST_REQUIRE_STR(LEAVES[3].literal_string, "HELLO");
/* Identifier predicates. */
TEST_REQUIRE_OK(akbasic_leaf_new_identifier(&LEAVES[4], AKBASIC_LEAF_IDENTIFIER_INT, "A#"));
TEST_REQUIRE(akbasic_leaf_is_identifier(&LEAVES[4]), "A# should be an identifier");
TEST_REQUIRE(!akbasic_leaf_is_literal(&LEAVES[4]), "A# is not a literal");
TEST_REQUIRE(akbasic_leaf_is_literal(&LEAVES[0]), "42 should be a literal");
TEST_REQUIRE(!akbasic_leaf_is_identifier(NULL), "NULL is not an identifier");
/* Binary and unary render in prefix form. */
TEST_REQUIRE_OK(akbasic_leaf_new_binary(&LEAVES[5], &LEAVES[4], AKBASIC_TOK_PLUS, &LEAVES[0]));
TEST_REQUIRE_OK(akbasic_leaf_to_string(&LEAVES[5], rendered, sizeof(rendered)));
TEST_REQUIRE_STR(rendered, "(+ A# 42)");
TEST_REQUIRE_OK(akbasic_leaf_new_unary(&LEAVES[6], AKBASIC_TOK_MINUS, &LEAVES[0]));
TEST_REQUIRE_OK(akbasic_leaf_to_string(&LEAVES[6], rendered, sizeof(rendered)));
TEST_REQUIRE_STR(rendered, "(- 42)");
TEST_REQUIRE_OK(akbasic_leaf_new_grouping(&LEAVES[7], &LEAVES[5]));
TEST_REQUIRE_OK(akbasic_leaf_to_string(&LEAVES[7], rendered, sizeof(rendered)));
TEST_REQUIRE_STR(rendered, "(group (+ A# 42))");
/* A deep clone copies the whole tree into pool storage. */
TEST_REQUIRE_OK(akbasic_leaf_clone(&LEAVES[7], &POOL, &copy));
TEST_REQUIRE(copy != NULL && copy != &LEAVES[7], "clone must be a distinct leaf");
TEST_REQUIRE(copy->expr != NULL && copy->expr != &LEAVES[5], "clone must be deep");
TEST_REQUIRE(copy->expr->left != NULL && copy->expr->left != &LEAVES[4],
"clone must recurse into .left");
TEST_REQUIRE_OK(akbasic_leaf_to_string(copy, rendered, sizeof(rendered)));
TEST_REQUIRE_STR(rendered, "(group (+ A# 42))");
/* Cloning NULL yields NULL, not an error: the reference does the same. */
TEST_REQUIRE_OK(akbasic_leaf_clone(NULL, &POOL, &copy));
TEST_REQUIRE(copy == NULL, "cloning NULL must yield NULL");
/* Pool exhaustion is diagnosed, not a buffer overrun. */
POOL.next = POOL.capacity;
TEST_REQUIRE_STATUS(akbasic_leaf_clone(&LEAVES[7], &POOL, &copy), AKBASIC_ERR_BOUNDS);
/* Constructors reject the nil arguments the reference rejects. */
TEST_REQUIRE_STATUS(akbasic_leaf_new_binary(&LEAVES[0], NULL, AKBASIC_TOK_PLUS, &LEAVES[1]),
AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_leaf_new_unary(&LEAVES[0], AKBASIC_TOK_MINUS, NULL),
AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_leaf_new_grouping(&LEAVES[0], NULL), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_leaf_new_branch(&LEAVES[0], NULL, NULL, NULL), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_leaf_new_comparison(&LEAVES[0], &LEAVES[1], AKBASIC_TOK_PLUS,
&LEAVES[2]),
AKBASIC_ERR_SYNTAX);
return akbasic_test_failures;
}

82
tests/harness.h Normal file
View File

@@ -0,0 +1,82 @@
/**
* @file harness.h
* @brief A runtime wired to memory buffers, for tests that need a live one.
*
* The runtime is `static` because it carries every pool the interpreter owns and
* will not fit on a default stack -- the same reason the driver's main() does it.
*/
#ifndef _AKBASIC_TEST_HARNESS_H_
#define _AKBASIC_TEST_HARNESS_H_
#include <stdio.h>
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/parser.h>
#include <akbasic/runtime.h>
#include <akbasic/scanner.h>
#include <akbasic/sink.h>
#include "testutil.h"
static akbasic_Runtime HARNESS_RUNTIME;
static akbasic_TextSink HARNESS_SINK;
static akbasic_StdioSink HARNESS_SINKSTATE;
static char HARNESS_OUTPUT[16384];
static FILE *HARNESS_OUT;
static FILE *HARNESS_IN;
/**
* @brief Bring up a runtime whose output lands in HARNESS_OUTPUT.
* @param input Text the sink will serve to readline; may be NULL for none.
*/
static akerr_ErrorContext AKERR_NOIGNORE *harness_start(const char *input)
{
PREPARE_ERROR(errctx);
memset(HARNESS_OUTPUT, 0, sizeof(HARNESS_OUTPUT));
HARNESS_OUT = fmemopen(HARNESS_OUTPUT, sizeof(HARNESS_OUTPUT), "w");
FAIL_ZERO_RETURN(errctx, (HARNESS_OUT != NULL), AKERR_IO, "could not open the output buffer");
setvbuf(HARNESS_OUT, NULL, _IONBF, 0);
if ( input != NULL ) {
HARNESS_IN = fmemopen((void *)(uintptr_t)input, strlen(input), "r");
FAIL_ZERO_RETURN(errctx, (HARNESS_IN != NULL), AKERR_IO, "could not open the input buffer");
} else {
HARNESS_IN = fmemopen((void *)(uintptr_t)"", 0, "r");
}
PASS(errctx, akbasic_sink_init_stdio(&HARNESS_SINK, &HARNESS_SINKSTATE, HARNESS_OUT, HARNESS_IN));
PASS(errctx, akbasic_runtime_init(&HARNESS_RUNTIME, &HARNESS_SINK));
SUCCEED_RETURN(errctx);
}
__attribute__((unused))
static void harness_stop(void)
{
if ( HARNESS_OUT != NULL ) {
fclose(HARNESS_OUT);
HARNESS_OUT = NULL;
}
if ( HARNESS_IN != NULL ) {
fclose(HARNESS_IN);
HARNESS_IN = NULL;
}
}
/** @brief Scan and parse one line, yielding its first statement. */
__attribute__((unused))
static akerr_ErrorContext AKERR_NOIGNORE *harness_parse(const char *line, akbasic_ASTLeaf **dest)
{
PREPARE_ERROR(errctx);
akbasic_Parser parser;
PASS(errctx, akbasic_scanner_zero(&HARNESS_RUNTIME));
PASS(errctx, akbasic_scanner_scan(&HARNESS_RUNTIME, line, NULL, 0));
PASS(errctx, akbasic_parser_init(&parser, &HARNESS_RUNTIME));
PASS(errctx, akbasic_parser_parse(&parser, dest));
SUCCEED_RETURN(errctx);
}
#endif // _AKBASIC_TEST_HARNESS_H_

View File

@@ -0,0 +1,81 @@
/**
* @file known_reference_defects.c
* @brief Asserts the *correct* contract for defects carried over from the reference.
*
* Registered in AKBASIC_KNOWN_FAILING_TESTS, so CTest expects it to fail. Every
* assertion here states what the interpreter *should* do, not what it currently
* does -- pinning current-but-wrong behaviour would turn the eventual fix into a
* test failure.
*
* When one of these is fixed, this test starts passing and CTest reports
* "unexpectedly passed". That is the cue to split the fixed assertion out into
* the corresponding suite in AKBASIC_TESTS and strike the item from TODO.md
* section 12.
*/
#include "harness.h"
int main(void)
{
akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *out = NULL;
char rendered[AKBASIC_MAX_STRING_LENGTH];
TEST_REQUIRE_OK(harness_start(NULL));
/*
* 12.12 -- subtraction stops after one operator, so `1 - 2 - 3` parses as
* `1 - 2` and abandons the rest of the line. It should fold left, the way
* addition does.
*/
TEST_REQUIRE_OK(harness_parse("A# = 1 - 2 - 3", &leaf));
TEST_REQUIRE_OK(akbasic_leaf_to_string(leaf, rendered, sizeof(rendered)));
TEST_REQUIRE_STR(rendered, "( A# (- (- 1 2) 3))");
/*
* 12.13 -- a unary-minus argument inflates the arity count, because the
* counter walks .right and a unary leaf keeps its operand there. ABS(-9)
* should be one argument.
*/
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(harness_parse("A# = ABS(-9)", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE_INT(out->intval, 9);
/*
* 12.14 -- a comparison operator in the final column of a line is dropped,
* because matchNextChar returns without setting a token type when it cannot
* peek past the end.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# =", NULL, 0));
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nexttoken, 2);
/*
* 12.15 -- hex literals do not survive the scanner. matchNumber lets 'x'
* through but not the digits after it, so the parser's base-16 branch is
* unreachable.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xff", NULL, 0));
TEST_REQUIRE_STR(HARNESS_RUNTIME.environment->tokens[1].lexeme, "0xff");
/*
* 12.16 -- "Reserved word in variable name" never fires, because the lexeme
* still carries its type suffix when the verb table is searched. PRINT$
* should be refused as a variable name.
*/
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0));
TEST_REQUIRE(HARNESS_RUNTIME.hasError, "PRINT$ should be refused as a variable name");
/*
* 12.10 -- a leading zero selects base 8, so `PRINT 010` prints 8 and
* `PRINT 08` will not parse. Commodore BASIC has no octal literals.
*/
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(harness_parse("A# = 010", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE_INT(out->intval, 10);
harness_stop();
return akbasic_test_failures;
}

111
tests/parser_commands.c Normal file
View File

@@ -0,0 +1,111 @@
/**
* @file parser_commands.c
* @brief Tests the verbs that carry their own parse path.
*/
#include "harness.h"
static void expect_command(const char *line, akbasic_LeafType wanttype, const char *wantname)
{
akbasic_ASTLeaf *leaf = NULL;
akerr_ErrorContext *e = NULL;
e = harness_parse(line, &leaf);
if ( e != NULL ) {
fprintf(stderr, "FAIL: \"%s\" did not parse: %s\n", line, e->message);
akbasic_test_failures += 1;
test_discard_error(e);
return;
}
TEST_REQUIRE(leaf != NULL, "\"%s\" produced no leaf", line);
if ( leaf == NULL ) {
return;
}
TEST_REQUIRE_INT(leaf->leaftype, wanttype);
if ( wantname != NULL ) {
TEST_REQUIRE_STR(leaf->identifier, wantname);
}
}
static void expect_parse_error(const char *line)
{
akbasic_ASTLeaf *leaf = NULL;
akerr_ErrorContext *e = NULL;
e = harness_parse(line, &leaf);
TEST_REQUIRE(e != NULL, "\"%s\" should not have parsed", line);
if ( e != NULL ) {
test_discard_error(e);
}
}
int main(void)
{
akbasic_ASTLeaf *leaf = NULL;
TEST_REQUIRE_OK(harness_start(NULL));
/* Plain verbs with an expression rval. */
expect_command("PRINT 1", AKBASIC_LEAF_COMMAND, "PRINT");
expect_command("GOTO 100", AKBASIC_LEAF_COMMAND, "GOTO");
expect_command("GOSUB 100", AKBASIC_LEAF_COMMAND, "GOSUB");
/* A verb with no rval at all must not fail. */
expect_command("RETURN", AKBASIC_LEAF_COMMAND, "RETURN");
expect_command("STOP", AKBASIC_LEAF_COMMAND, "STOP");
/* Immediate commands get their own leaf type. */
expect_command("QUIT", AKBASIC_LEAF_COMMAND_IMMEDIATE, "QUIT");
expect_command("LIST", AKBASIC_LEAF_COMMAND_IMMEDIATE, "LIST");
expect_command("RUN", AKBASIC_LEAF_COMMAND_IMMEDIATE, "RUN");
/* Verbs with their own parse path. */
expect_command("LABEL LOOPTOP", AKBASIC_LEAF_COMMAND, "LABEL");
expect_command("DIM A#(5)", AKBASIC_LEAF_COMMAND, "DIM");
expect_command("DATA 1, 2, 3", AKBASIC_LEAF_COMMAND, "DATA");
expect_command("READ A#, B#", AKBASIC_LEAF_COMMAND, "READ");
expect_command("POKE 100, 1", AKBASIC_LEAF_COMMAND, "POKE");
expect_command("INPUT \"NAME? \" A$", AKBASIC_LEAF_COMMAND, "INPUT");
/* LET parses as a bare assignment. */
expect_command("LET A# = 1", AKBASIC_LEAF_BINARY, NULL);
/* IF produces a BRANCH, not a COMMAND. */
expect_command("IF 1 == 1 THEN PRINT 1", AKBASIC_LEAF_BRANCH, NULL);
TEST_REQUIRE_OK(harness_parse("IF 1 == 1 THEN PRINT 1 ELSE PRINT 2", &leaf));
TEST_REQUIRE(leaf != NULL && leaf->left != NULL && leaf->right != NULL,
"IF ... THEN ... ELSE must fill both branches");
/* FOR installs a loop environment and returns the assignment. */
{
akbasic_Environment *before = HARNESS_RUNTIME.environment;
TEST_REQUIRE_OK(harness_parse("FOR I# = 1 TO 5", &leaf));
TEST_REQUIRE(HARNESS_RUNTIME.environment != before,
"FOR must install a new environment");
TEST_REQUIRE(HARNESS_RUNTIME.environment->forToLeaf != NULL,
"FOR must record its TO expression");
TEST_REQUIRE(HARNESS_RUNTIME.environment->forStepLeaf != NULL,
"FOR must default its STEP to 1");
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
}
{
akbasic_Environment *before = HARNESS_RUNTIME.environment;
TEST_REQUIRE_OK(harness_parse("FOR I# = 1 TO 10 STEP 2", &leaf));
TEST_REQUIRE(HARNESS_RUNTIME.environment != before, "FOR STEP must install an environment");
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
}
/* Malformed FOR is rejected at parse time. */
expect_parse_error("FOR I# = 1");
expect_parse_error("FOR I# = 1 TO 5 STEP");
expect_parse_error("FOR 1 TO 5");
/* Other malformed forms. */
expect_parse_error("IF 1 == 1 PRINT 1");
expect_parse_error("LABEL 5");
expect_parse_error("READ 1");
expect_parse_error("DATA A#");
harness_stop();
return akbasic_test_failures;
}

106
tests/parser_expressions.c Normal file
View File

@@ -0,0 +1,106 @@
/**
* @file parser_expressions.c
* @brief Tests the expression grammar via the leaf renderer.
*/
#include "harness.h"
/* Parse a line and assert the shape of the tree it produced. */
static void expect_tree(const char *line, const char *want)
{
akbasic_ASTLeaf *leaf = NULL;
char rendered[AKBASIC_MAX_STRING_LENGTH];
akerr_ErrorContext *e = NULL;
e = harness_parse(line, &leaf);
if ( e != NULL ) {
fprintf(stderr, "FAIL: \"%s\" did not parse: %s\n", line, e->message);
akbasic_test_failures += 1;
test_discard_error(e);
return;
}
e = akbasic_leaf_to_string(leaf, rendered, sizeof(rendered));
if ( e != NULL ) {
test_discard_error(e);
akbasic_test_failures += 1;
return;
}
TEST_REQUIRE(strcmp(rendered, want) == 0,
"\"%s\": expected %s, got %s", line, want, rendered);
}
static void expect_parse_error(const char *line)
{
akbasic_ASTLeaf *leaf = NULL;
akerr_ErrorContext *e = NULL;
e = harness_parse(line, &leaf);
TEST_REQUIRE(e != NULL, "\"%s\" should not have parsed", line);
if ( e != NULL ) {
test_discard_error(e);
}
}
int main(void)
{
akbasic_ASTLeaf *leaf = NULL;
TEST_REQUIRE_OK(harness_start(NULL));
/*
* An assignment renders with an empty operator: the reference's
* operatorToStr() has a case for EQUAL but none for ASSIGNMENT, so it prints
* "( A# ...)". Reproduced, so these expectations look odd on purpose.
*/
/* Multiplication binds tighter than addition. */
expect_tree("PRINT 1 + 2 * 3", "(PRINT)");
expect_tree("A# = 1 + 2 * 3", "( A# (+ 1 (* 2 3)))");
expect_tree("A# = 1 * 2 + 3", "( A# (+ (* 1 2) 3))");
expect_tree("A# = 8 / 2 / 2", "( A# (/ (/ 8 2) 2))");
/* Grouping overrides precedence. */
expect_tree("A# = (1 + 2) * 3", "( A# (* (group (+ 1 2)) 3))");
/* Unary minus and NOT. */
expect_tree("A# = -5", "( A# (- 5))");
expect_tree("A# = NOT 1", "( A# (NOT 1))");
/* Comparisons sit below the arithmetic. */
expect_tree("A# = 1 + 1 == 2", "( A# (= (+ 1 1) 2))");
expect_tree("A# = 1 <> 2", "( A# (<> 1 2))");
/* AND and OR sit below comparison. */
expect_tree("A# = 1 == 1 AND 2 == 2", "( A# (AND (= 1 1) (= 2 2)))");
/* Exponent. */
expect_tree("A# = 2 ^ 3", "( A# (^ 2 3))");
/*
* Addition loops over its operator; subtraction returns after one. So
* `1 + 2 + 3` folds left, while `1 - 2 - 3` parses only `1 - 2` and leaves
* `- 3` in the token stream for the statement loop to pick up as a second
* statement. That silently computes the wrong answer -- TODO.md section 12
* item 12 -- and is pinned here so the fix is a deliberate change to this
* assertion and not an accident.
*/
expect_tree("A# = 1 + 2 + 3", "( A# (+ (+ 1 2) 3))");
expect_tree("A# = 1 - 2 - 3", "( A# (- 1 2))");
/* An array reference parses as an identifier carrying a subscript list. */
TEST_REQUIRE_OK(harness_parse("A#(2) = 1", &leaf));
TEST_REQUIRE(leaf != NULL && leaf->left != NULL, "assignment must have an lvalue");
if ( leaf != NULL && leaf->left != NULL ) {
TEST_REQUIRE_INT(leaf->left->leaftype, AKBASIC_LEAF_IDENTIFIER_INT);
TEST_REQUIRE(akbasic_leaf_first_subscript(leaf->left) != NULL,
"A#(2) must carry an array subscript");
}
/* Failure paths. */
expect_parse_error("A# = ");
expect_parse_error("A# = (1 + 2");
expect_parse_error("A# = *");
harness_stop();
return akbasic_test_failures;
}

143
tests/runtime_evaluate.c Normal file
View File

@@ -0,0 +1,143 @@
/**
* @file runtime_evaluate.c
* @brief Tests the evaluator against hand-built and hand-parsed trees.
*/
#include "harness.h"
/* Parse an expression-bearing line and evaluate what it produced. */
static akerr_ErrorContext AKERR_NOIGNORE *eval_line(const char *line, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *leaf = NULL;
PASS(errctx, akbasic_environment_zero(HARNESS_RUNTIME.environment));
PASS(errctx, harness_parse(line, &leaf));
PASS(errctx, akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, dest));
SUCCEED_RETURN(errctx);
}
static void expect_int(const char *line, int64_t want)
{
akbasic_Value *out = NULL;
akerr_ErrorContext *e = eval_line(line, &out);
if ( e != NULL ) {
fprintf(stderr, "FAIL: \"%s\": %s\n", line, e->message);
akbasic_test_failures += 1;
test_discard_error(e);
return;
}
TEST_REQUIRE(out->valuetype == AKBASIC_TYPE_INTEGER,
"\"%s\" produced type %d, expected integer", line, (int)out->valuetype);
TEST_REQUIRE(out->intval == want,
"\"%s\" produced %lld, expected %lld",
line, (long long)out->intval, (long long)want);
}
static void expect_bool(const char *line, bool want)
{
akbasic_Value *out = NULL;
akerr_ErrorContext *e = eval_line(line, &out);
if ( e != NULL ) {
fprintf(stderr, "FAIL: \"%s\": %s\n", line, e->message);
akbasic_test_failures += 1;
test_discard_error(e);
return;
}
TEST_REQUIRE(akbasic_value_is_true(out) == want,
"\"%s\" expected %s", line, (want ? "true" : "false"));
}
int main(void)
{
akbasic_Value *out = NULL;
akerr_ErrorContext *e = NULL;
TEST_REQUIRE_OK(harness_start(NULL));
/* Literals and arithmetic. */
expect_int("A# = 42", 42);
expect_int("A# = 1 + 2 * 3", 7);
expect_int("A# = (1 + 2) * 3", 9);
expect_int("A# = -5 + 10", 5);
expect_int("A# = 8 / 2", 4);
/* Identifiers read back what was assigned. */
expect_int("A# = 7", 7);
expect_int("B# = A# + 1", 8);
/*
* Comparisons yield BASIC booleans. They are evaluated bare rather than
* assigned: assign() accepts only INTEGER or FLOAT into a `#` variable, so
* `A# = 1 == 1` is "Incompatible types in variable assignment" -- which is
* also why IF works on the comparison directly and never through a variable.
*
* They are wrapped in parens because a bare leading integer in token
* position 0 is consumed as a *line number*, not as a literal.
*/
expect_bool("(1 == 1)", true);
expect_bool("(1 == 2)", false);
expect_bool("(2 > 1)", true);
expect_bool("(1 <> 2)", true);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(eval_line("A# = 1 == 1", &out), AKBASIC_ERR_TYPE);
/* Bitwise operators. */
expect_int("A# = 12 AND 10", 8);
expect_int("A# = 12 OR 10", 14);
/* Arrays: assign through a subscript and read it back. */
{
akbasic_ASTLeaf *leaf = NULL;
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(harness_parse("DIM C#(4)", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
}
expect_int("C#(2) = 99", 99);
expect_int("D# = C#(2)", 99);
expect_int("D# = C#(0)", 0);
/* An out-of-bounds subscript raises with the reference's message. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
e = eval_line("D# = C#(9)", &out);
TEST_REQUIRE(e != NULL, "an out-of-bounds read must raise");
if ( e != NULL ) {
TEST_REQUIRE_STR(e->message,
"Variable index access out of bounds at dimension 0: 9 (max 3)");
test_discard_error(e);
}
/*
* Built-in functions dispatch through the table.
*
* Note the arguments are never negative *literals*: a unary-minus leaf uses
* its .right for the operand, and the parser counts arity by walking .right,
* so `ABS(-9)` reports two arguments and is rejected. TODO.md section 12
* item 13. The golden corpus sidesteps it the same way -- sgn.bas assigns
* -1 to a variable first.
*/
expect_int("N# = -9", -9);
expect_int("A# = ABS(N#)", 9);
expect_int("A# = SGN(N#)", -1);
expect_int("A# = LEN(\"HELLO\")", 5);
expect_int("A# = SHL(1, 4)", 16);
expect_int("A# = SHR(16, 4)", 1);
expect_int("A# = XOR(12, 10)", 6);
expect_int("A# = MOD(7, 3)", 1);
expect_int("A# = INSTR(\"HELLO\", \"LL\")", 2);
expect_int("A# = INSTR(\"HELLO\", \"ZZ\")", -1);
/* An unknown verb is diagnosed rather than silently ignored. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, NULL, &out),
AKERR_NULLPOINTER);
/* A label resolves to its line number through a bare identifier. */
TEST_REQUIRE_OK(akbasic_environment_set_label(HARNESS_RUNTIME.environment, "TOP", 30));
expect_int("A# = TOP", 30);
harness_stop();
return akbasic_test_failures;
}

245
tests/runtime_verbs.c Normal file
View File

@@ -0,0 +1,245 @@
/**
* @file runtime_verbs.c
* @brief Tests the verbs the golden corpus never reaches.
*
* LIST, DELETE, AUTO, INPUT, DLOAD, DSAVE, EXIT, STOP and the memory verbs are
* all real code with no `.bas`/`.txt` pair behind them, because the reference's
* own corpus never exercises them either. They are driven through a whole
* runtime here rather than unit-tested in pieces: most of them exist to mutate
* runtime state, and that is the thing worth asserting.
*/
#include <stdio.h>
#include <unistd.h>
#include "harness.h"
/* Run one line as though it had been typed at the REPL. */
static akerr_ErrorContext AKERR_NOIGNORE *run_line(const char *line)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *out = NULL;
PASS(errctx, akbasic_environment_zero(HARNESS_RUNTIME.environment));
PASS(errctx, harness_parse(line, &leaf));
PASS(errctx, akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
SUCCEED_RETURN(errctx);
}
/* Load a program into the source table the way RUNSTREAM would. */
static void load(const char *lines[], int count)
{
char scanned[AKBASIC_MAX_LINE_LENGTH];
int i = 0;
for ( i = 0; i < count; i++ ) {
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, lines[i], scanned, sizeof(scanned)));
TEST_REQUIRE_OK(akbasic_runtime_store_line(&HARNESS_RUNTIME,
HARNESS_RUNTIME.environment->lineno, scanned));
}
}
static void clear_output(void)
{
memset(HARNESS_OUTPUT, 0, sizeof(HARNESS_OUTPUT));
rewind(HARNESS_OUT);
}
int main(void)
{
static const char *PROGRAM[] = {
"10 PRINT \"ONE\"",
"20 PRINT \"TWO\"",
"30 PRINT \"THREE\"",
};
char tmpname[] = "/tmp/akbasic_dsave_XXXXXX";
char buffer[256];
akbasic_Value *out = NULL;
FILE *saved = NULL;
int fd = -1;
TEST_REQUIRE_OK(harness_start("42\n"));
/* PRINT with no argument emits a bare newline. */
clear_output();
TEST_REQUIRE_OK(run_line("PRINT"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "\n");
/*
* LET does nothing; the assignment already happened during evaluation. A
* bare LET with no assignment after it is a parse error, in the reference
* too -- its parse path is just assignment(), which needs an expression.
*/
TEST_REQUIRE_OK(run_line("LET A# = 5"));
TEST_REQUIRE_ANY_ERROR(run_line("LET"));
/* LIST prints the program with its line numbers restored. */
load(PROGRAM, 3);
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT,
"10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
/* LIST n lists from n to the end. */
clear_output();
TEST_REQUIRE_OK(run_line("LIST 20"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
/* LIST -n lists from the start through n. */
clear_output();
TEST_REQUIRE_OK(run_line("LIST -20"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n");
/* LIST n-n lists an inclusive range. */
clear_output();
TEST_REQUIRE_OK(run_line("LIST 20-30"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
/* DSAVE writes the program out; DLOAD reads it back. */
fd = mkstemp(tmpname);
TEST_REQUIRE(fd >= 0, "could not create a temporary file");
if ( fd >= 0 ) {
close(fd);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
snprintf(buffer, sizeof(buffer), "DSAVE \"%s\"", tmpname);
TEST_REQUIRE_OK(run_line(buffer));
saved = fopen(tmpname, "r");
TEST_REQUIRE(saved != NULL, "DSAVE produced no file");
if ( saved != NULL ) {
TEST_REQUIRE(fgets(buffer, sizeof(buffer), saved) != NULL, "DSAVE wrote nothing");
TEST_REQUIRE_STR(buffer, "10 PRINT \"ONE\"\n");
fclose(saved);
}
/* DELETE with no range clears the whole program. */
TEST_REQUIRE_OK(run_line("DELETE"));
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
/* DLOAD brings it back. */
snprintf(buffer, sizeof(buffer), "DLOAD \"%s\"", tmpname);
TEST_REQUIRE_OK(run_line(buffer));
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT,
"10 PRINT \"ONE\"\n20 PRINT \"TWO\"\n30 PRINT \"THREE\"\n");
remove(tmpname);
}
/* DELETE n-n removes an inclusive range. */
TEST_REQUIRE_OK(run_line("DELETE 20-30"));
clear_output();
TEST_REQUIRE_OK(run_line("LIST"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "10 PRINT \"ONE\"\n");
/* A missing file is an error, not a crash. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_ANY_ERROR(run_line("DLOAD \"/nonexistent/akbasic/nope.bas\""));
/* An empty filename is refused before it reaches aksl_fopen. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("DLOAD \"\""), AKBASIC_ERR_VALUE);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("DLOAD 5"), AKBASIC_ERR_TYPE);
/* AUTO sets and clears the line-numbering increment. */
TEST_REQUIRE_OK(run_line("AUTO 10"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 10);
TEST_REQUIRE_OK(run_line("AUTO"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.autoLineNumber, 0);
/* INPUT prompts and stores. The harness input stream holds "42". */
clear_output();
TEST_REQUIRE_OK(run_line("INPUT \"AGE? \" N#"));
TEST_REQUIRE_STR(HARNESS_OUTPUT, "AGE? ");
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
{
akbasic_ASTLeaf *leaf = NULL;
TEST_REQUIRE_OK(harness_parse("A# = N#", &leaf));
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
TEST_REQUIRE_INT(out->intval, 42);
}
/* Mode transitions. */
TEST_REQUIRE_OK(run_line("STOP"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_REPL);
TEST_REQUIRE_OK(run_line("RUN"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_RUN);
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 0);
TEST_REQUIRE_OK(run_line("RUN 20"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 20);
/*
* QUIT sets a mode. It must not call exit(): a host game decides what
* quitting means, and this test would not survive it doing otherwise.
*/
TEST_REQUIRE_OK(run_line("QUIT"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.mode, AKBASIC_MODE_QUIT);
TEST_REQUIRE_OK(akbasic_runtime_set_mode(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
/* State errors: verbs used outside the structure they need. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("RETURN"), AKBASIC_ERR_STATE);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("EXIT"), AKBASIC_ERR_STATE);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("NEXT I#"), AKBASIC_ERR_STATE);
/* GOTO and GOSUB want an integer. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("GOTO \"nope\""), AKBASIC_ERR_TYPE);
/* GOTO sets the next line. */
TEST_REQUIRE_OK(run_line("GOTO 30"));
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 30);
/* GOSUB pushes an environment; RETURN pops it and restores the line. */
{
akbasic_Environment *before = HARNESS_RUNTIME.environment;
TEST_REQUIRE_OK(akbasic_environment_zero(before));
before->lineno = 100;
TEST_REQUIRE_OK(run_line("GOSUB 500"));
TEST_REQUIRE(HARNESS_RUNTIME.environment != before, "GOSUB must push an environment");
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->nextline, 500);
TEST_REQUIRE_INT(HARNESS_RUNTIME.environment->gosubReturnLine, 101);
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("RETURN"));
TEST_REQUIRE(HARNESS_RUNTIME.environment == before, "RETURN must pop back");
TEST_REQUIRE_INT(before->nextline, 101);
}
/* LABEL records the current line under a name. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
HARNESS_RUNTIME.environment->lineno = 70;
TEST_REQUIRE_OK(run_line("LABEL AGAIN"));
{
int64_t where = 0;
TEST_REQUIRE_OK(akbasic_environment_get_label(HARNESS_RUNTIME.environment, "AGAIN", &where));
TEST_REQUIRE_INT(where, 70);
}
/* POKE, PEEK and POINTER round-trip through real memory. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("V# = 255"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("P# = POINTER(V#)"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("Q# = PEEK(P#)"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("POKE P#, 127"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("R# = POINTERVAR(V#)"));
/* PEEK through a null address is refused rather than segfaulting. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_OK(run_line("Z# = 0"));
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(run_line("Y# = PEEK(Z#)"), AKBASIC_ERR_VALUE);
harness_stop();
return akbasic_test_failures;
}

158
tests/scanner_tokens.c Normal file
View File

@@ -0,0 +1,158 @@
/**
* @file scanner_tokens.c
* @brief Tests the line tokenizer.
*/
#include "harness.h"
static akbasic_Environment *env(void)
{
return HARNESS_RUNTIME.environment;
}
/* Scan a line and assert the token type at a given index. */
static void expect_token(int index, akbasic_TokenType want, const char *lexeme)
{
TEST_REQUIRE(index < env()->nexttoken,
"expected a token at index %d, only %d were produced",
index, env()->nexttoken);
if ( index >= env()->nexttoken ) {
return;
}
TEST_REQUIRE_INT(env()->tokens[index].tokentype, want);
if ( lexeme != NULL ) {
TEST_REQUIRE_STR(env()->tokens[index].lexeme, lexeme);
}
}
int main(void)
{
char rewritten[AKBASIC_MAX_LINE_LENGTH];
TEST_REQUIRE_OK(harness_start(NULL));
/* Single-character operators. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "+-*/^(),", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 8);
expect_token(0, AKBASIC_TOK_PLUS, "+");
expect_token(1, AKBASIC_TOK_MINUS, "-");
expect_token(2, AKBASIC_TOK_STAR, "*");
expect_token(3, AKBASIC_TOK_LEFT_SLASH, "/");
expect_token(4, AKBASIC_TOK_CARAT, "^");
expect_token(5, AKBASIC_TOK_LEFT_PAREN, "(");
expect_token(6, AKBASIC_TOK_RIGHT_PAREN, ")");
expect_token(7, AKBASIC_TOK_COMMA, ",");
/*
* Two-character comparisons, and the one-character fallbacks. The trailing
* space is not decoration: matchNextChar cannot peek past the end of the
* line, and when it fails it returns without setting a token type, so a
* comparison operator in the final column is silently dropped. TODO.md
* section 12 item 14.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "<= <> >= < > == = ", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 7);
expect_token(0, AKBASIC_TOK_LESS_THAN_EQUAL, NULL);
expect_token(1, AKBASIC_TOK_NOT_EQUAL, NULL);
expect_token(2, AKBASIC_TOK_GREATER_THAN_EQUAL, NULL);
expect_token(3, AKBASIC_TOK_LESS_THAN, NULL);
expect_token(4, AKBASIC_TOK_GREATER_THAN, NULL);
expect_token(5, AKBASIC_TOK_EQUAL, NULL);
expect_token(6, AKBASIC_TOK_ASSIGNMENT, NULL);
/* Without the trailing space the final operator vanishes. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# =", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 1);
/* Literals. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 1.5", NULL, 0));
expect_token(0, AKBASIC_TOK_COMMAND, "PRINT");
expect_token(1, AKBASIC_TOK_LITERAL_FLOAT, "1.5");
/*
* A hex literal does not survive the scanner: matchNumber allows 'x' through
* but not the hex digits after it, so "0xff" lexes as "0x" and the "ff" is
* scanned as an identifier. The parser's base-16 branch is therefore
* unreachable. TODO.md section 12 item 15.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 0xff", NULL, 0));
expect_token(1, AKBASIC_TOK_LITERAL_INT, "0x");
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT \"HELLO\"", NULL, 0));
expect_token(1, AKBASIC_TOK_LITERAL_STRING, "HELLO");
/* An empty string literal really is empty, not a stray quote. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT \"\"", NULL, 0));
expect_token(1, AKBASIC_TOK_LITERAL_STRING, "");
/* Identifier suffixes pick the token type. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# B% C$ D", NULL, 0));
expect_token(0, AKBASIC_TOK_IDENTIFIER_INT, "A#");
expect_token(1, AKBASIC_TOK_IDENTIFIER_FLOAT, "B%");
expect_token(2, AKBASIC_TOK_IDENTIFIER_STRING, "C$");
expect_token(3, AKBASIC_TOK_IDENTIFIER, "D");
/* Verbs are case-insensitive; a variable is not a verb. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "print goto", NULL, 0));
expect_token(0, AKBASIC_TOK_COMMAND, "print");
expect_token(1, AKBASIC_TOK_COMMAND, "goto");
/* Reserved words get their own token types, not COMMAND. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "A# AND B# OR NOT C#", NULL, 0));
expect_token(1, AKBASIC_TOK_AND, NULL);
expect_token(3, AKBASIC_TOK_OR, NULL);
expect_token(4, AKBASIC_TOK_NOT, NULL);
/*
* The line-number rule: an integer in token position 0 is consumed, not
* emitted, and the line is rewritten to what follows it with leading spaces
* stripped. The REPL stores the rewritten line as the program text, so this
* is load-bearing rather than cosmetic.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "10 PRINT 1", rewritten, sizeof(rewritten)));
TEST_REQUIRE_STR(rewritten, "PRINT 1");
TEST_REQUIRE_INT(env()->lineno, 10);
expect_token(0, AKBASIC_TOK_COMMAND, "PRINT");
expect_token(1, AKBASIC_TOK_LITERAL_INT, "1");
/* An integer that is *not* in position 0 stays a literal. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT 10", NULL, 0));
expect_token(1, AKBASIC_TOK_LITERAL_INT, "10");
/* REM ends the line: nothing after it becomes a token. */
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "REM PRINT \"NOT SEEN\"", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 0);
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "10 REM A COMMENT", NULL, 0));
TEST_REQUIRE_INT(env()->nexttoken, 0);
TEST_REQUIRE_INT(env()->lineno, 10);
/*
* The "Reserved word in variable name" check never fires: the lexeme still
* carries its suffix when the verb table is searched, so "PRINT$" misses and
* becomes an ordinary string variable. Pinned as it stands -- TODO.md
* section 12 item 16.
*/
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT$ = 1", NULL, 0));
TEST_REQUIRE(!HARNESS_RUNTIME.hasError,
"PRINT$ is currently accepted as a variable name; see TODO.md 12.16");
expect_token(0, AKBASIC_TOK_IDENTIFIER_STRING, "PRINT$");
/* An unknown character is diagnosed. */
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "PRINT ~", NULL, 0));
TEST_REQUIRE(HARNESS_RUNTIME.hasError, "an unknown token must be diagnosed");
/* A malformed number is diagnosed rather than silently becoming zero. */
HARNESS_RUNTIME.hasError = false;
TEST_REQUIRE_OK(akbasic_scanner_scan(&HARNESS_RUNTIME, "1x2 PRINT 1", NULL, 0));
TEST_REQUIRE(HARNESS_RUNTIME.hasError,
"a malformed line number must be diagnosed, not converted to 0");
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "INTEGER CONVERSION ON '1x2'") != NULL,
"expected the reference's conversion message, got: %s", HARNESS_OUTPUT);
TEST_REQUIRE_STATUS(akbasic_scanner_scan(&HARNESS_RUNTIME, NULL, NULL, 0), AKERR_NULLPOINTER);
harness_stop();
return akbasic_test_failures;
}

78
tests/sink_stdio.c Normal file
View File

@@ -0,0 +1,78 @@
/**
* @file sink_stdio.c
* @brief Tests the stdio sink, byte for byte.
*
* The double newline on an error line is the assertion that matters: the caller's
* message already ends in \n and writeln adds another, and
* tests/language/array_outofbounds.txt ends in 0a 0a because of it. See TODO.md
* section 1.8.
*/
#include <stdio.h>
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/sink.h>
#include "testutil.h"
static char OUTPUT[1024];
int main(void)
{
akbasic_TextSink sink;
akbasic_StdioSink state;
FILE *out = NULL;
FILE *in = NULL;
char line[64];
bool eof = false;
TEST_REQUIRE_OK(akbasic_error_register());
memset(OUTPUT, 0, sizeof(OUTPUT));
out = fmemopen(OUTPUT, sizeof(OUTPUT), "w");
TEST_REQUIRE(out != NULL, "could not open the output buffer");
setvbuf(out, NULL, _IONBF, 0);
in = fmemopen((void *)(uintptr_t)"first\nsecond\n", 13, "r");
TEST_REQUIRE(in != NULL, "could not open the input buffer");
TEST_REQUIRE_OK(akbasic_sink_init_stdio(&sink, &state, out, in));
/* write adds nothing; writeln adds exactly one newline. */
TEST_REQUIRE_OK(sink.write(&sink, "AB"));
TEST_REQUIRE_OK(sink.write(&sink, "CD"));
TEST_REQUIRE_OK(sink.writeln(&sink, "EF"));
TEST_REQUIRE_STR(OUTPUT, "ABCDEF\n");
/*
* The error-line shape: a message that already ends in \n, handed to writeln,
* produces two. Reproducing this exactly is what makes the golden corpus
* pass.
*/
memset(OUTPUT, 0, sizeof(OUTPUT));
rewind(out);
TEST_REQUIRE_OK(sink.writeln(&sink, "? 20 : RUNTIME ERROR something\n"));
TEST_REQUIRE_STR(OUTPUT, "? 20 : RUNTIME ERROR something\n\n");
TEST_REQUIRE_INT(strlen(OUTPUT), 32);
TEST_REQUIRE_INT(OUTPUT[30], '\n');
TEST_REQUIRE_INT(OUTPUT[31], '\n');
/* readline strips the terminator and reports EOF without raising. */
TEST_REQUIRE_OK(sink.readline(&sink, line, sizeof(line), &eof));
TEST_REQUIRE(!eof, "the first line is not EOF");
TEST_REQUIRE_STR(line, "first");
TEST_REQUIRE_OK(sink.readline(&sink, line, sizeof(line), &eof));
TEST_REQUIRE_STR(line, "second");
TEST_REQUIRE_OK(sink.readline(&sink, line, sizeof(line), &eof));
TEST_REQUIRE(eof, "running off the end must set eof, not raise");
/* Argument validation. */
TEST_REQUIRE_STATUS(sink.write(&sink, NULL), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(sink.readline(&sink, line, 1, &eof), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_sink_init_stdio(NULL, &state, out, in), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_sink_init_stdio(&sink, NULL, out, in), AKERR_NULLPOINTER);
fclose(out);
fclose(in);
return akbasic_test_failures;
}

75
tests/symtab.c Normal file
View File

@@ -0,0 +1,75 @@
/**
* @file symtab.c
* @brief Tests the fixed-capacity open-addressed symbol table.
*/
#include <stdio.h>
#include <akbasic/error.h>
#include <akbasic/symtab.h>
#include "testutil.h"
static akbasic_SymbolTable TABLE;
int main(void)
{
void *value = NULL;
int64_t ivalue = 0;
char key[32];
int marker_a = 1;
int marker_b = 2;
int i = 0;
TEST_REQUIRE_OK(akbasic_error_register());
TEST_REQUIRE_OK(akbasic_symtab_init(&TABLE, 16));
/* Round-trip both payloads. */
TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, "A#", &marker_a, 42));
TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, "A#", &value, &ivalue));
TEST_REQUIRE(value == &marker_a, "wrong pointer payload");
TEST_REQUIRE_INT(ivalue, 42);
/* Replacing an existing key must not grow the table. */
TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, "A#", &marker_b, 43));
TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, "A#", &value, &ivalue));
TEST_REQUIRE(value == &marker_b, "replacement did not take");
TEST_REQUIRE_INT(ivalue, 43);
TEST_REQUIRE_INT(TABLE.count, 1);
/* A miss is AKERR_KEY, not a crash and not a silent NULL. */
TEST_REQUIRE_STATUS(akbasic_symtab_get(&TABLE, "B$", &value, &ivalue), AKERR_KEY);
/*
* Collisions. Sixteen distinct keys in a sixteen-slot table guarantees the
* probe sequence is exercised, and the last insert has exactly one slot to
* find.
*/
TEST_REQUIRE_OK(akbasic_symtab_clear(&TABLE));
for ( i = 0; i < 16; i++ ) {
snprintf(key, sizeof(key), "V%d#", i);
TEST_REQUIRE_OK(akbasic_symtab_set(&TABLE, key, NULL, i));
}
for ( i = 0; i < 16; i++ ) {
snprintf(key, sizeof(key), "V%d#", i);
TEST_REQUIRE_OK(akbasic_symtab_get(&TABLE, key, NULL, &ivalue));
TEST_REQUIRE_INT(ivalue, i);
}
/* Seventeenth key into a sixteen-slot table: full, and it says so. */
TEST_REQUIRE_STATUS(akbasic_symtab_set(&TABLE, "OVERFLOW#", NULL, 0), AKBASIC_ERR_BOUNDS);
/* Clear keeps the capacity but drops the contents. */
TEST_REQUIRE_OK(akbasic_symtab_clear(&TABLE));
TEST_REQUIRE_INT(TABLE.count, 0);
TEST_REQUIRE_INT(TABLE.capacity, 16);
TEST_REQUIRE_STATUS(akbasic_symtab_get(&TABLE, "V0#", NULL, NULL), AKERR_KEY);
/* Argument validation. */
TEST_REQUIRE_STATUS(akbasic_symtab_init(NULL, 16), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_symtab_init(&TABLE, 0), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(akbasic_symtab_init(&TABLE, AKBASIC_SYMTAB_MAX_SLOTS + 1),
AKBASIC_ERR_BOUNDS);
return akbasic_test_failures;
}

109
tests/testutil.h Normal file
View File

@@ -0,0 +1,109 @@
/**
* @file testutil.h
* @brief Shared assertions for the akbasic CTest programs.
*
* A test passes by exiting zero and fails by exiting non-zero with something
* informative on stderr.
*
* TEST_ASSERT and friends expand to a `break`, so they belong directly inside an
* ATTEMPT block, never inside a loop nested in one -- the same rule that governs
* CATCH and the FAIL_*_BREAK macros. TEST_REQUIRE is the return-based form for
* use inside loops and outside ATTEMPT blocks.
*/
#ifndef _AKBASIC_TESTUTIL_H_
#define _AKBASIC_TESTUTIL_H_
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <akerror.h>
/** Count of failures recorded by TEST_REQUIRE; main() returns it. */
static int akbasic_test_failures = 0;
/*
* Mark an error handled and hand it back to the pool. Assigning the result is
* what satisfies AKERR_NOIGNORE -- a (void) cast does not, and libakerror's
* IGNORE() would log to stderr and pollute the test output.
*/
static void test_discard_error(akerr_ErrorContext *e)
{
akerr_ErrorContext *released = NULL;
if ( e == NULL ) {
return;
}
e->handled = true;
released = akerr_release_error(e);
(void)released;
}
#define TEST_REQUIRE(__cond, ...) \
do { \
if ( !(__cond) ) { \
fprintf(stderr, "%s:%d: FAIL: ", __FILE__, __LINE__); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
akbasic_test_failures += 1; \
} \
} while ( 0 )
#define TEST_REQUIRE_STR(__got, __want) \
TEST_REQUIRE(strcmp((__got), (__want)) == 0, \
"expected \"%s\", got \"%s\"", (__want), (__got))
#define TEST_REQUIRE_INT(__got, __want) \
TEST_REQUIRE((long long)(__got) == (long long)(__want), \
"expected %lld, got %lld", \
(long long)(__want), (long long)(__got))
#define TEST_REQUIRE_FEQ(__got, __want) \
TEST_REQUIRE(fabs((double)(__got) - (double)(__want)) < 1e-9, \
"expected %f, got %f", (double)(__want), (double)(__got))
/*
* Assert that a call succeeded. Releases the context on failure so the pool does
* not drain across a long test.
*/
#define TEST_REQUIRE_OK(__call) \
do { \
akerr_ErrorContext *__e = (__call); \
if ( __e != NULL && __e->status != 0 ) { \
fprintf(stderr, "%s:%d: FAIL: %s: unexpected error %d (%s): %s\n", \
__FILE__, __LINE__, #__call, __e->status, \
akerr_name_for_status(__e->status, NULL), __e->message); \
akbasic_test_failures += 1; \
} \
test_discard_error(__e); \
} while ( 0 )
/** Assert that a call failed with a specific status. */
#define TEST_REQUIRE_STATUS(__call, __status) \
do { \
akerr_ErrorContext *__e = (__call); \
if ( __e == NULL || __e->status != (__status) ) { \
fprintf(stderr, "%s:%d: FAIL: %s: expected status %d (%s), got %d\n", \
__FILE__, __LINE__, #__call, (int)(__status), \
akerr_name_for_status((__status), NULL), \
(__e == NULL ? 0 : __e->status)); \
akbasic_test_failures += 1; \
} \
test_discard_error(__e); \
} while ( 0 )
/** Assert that a call failed, without caring which status. */
#define TEST_REQUIRE_ANY_ERROR(__call) \
do { \
akerr_ErrorContext *__e = (__call); \
if ( __e == NULL || __e->status == 0 ) { \
fprintf(stderr, "%s:%d: FAIL: %s: expected an error, got success\n", \
__FILE__, __LINE__, #__call); \
akbasic_test_failures += 1; \
} \
test_discard_error(__e); \
} while ( 0 )
#endif // _AKBASIC_TESTUTIL_H_

126
tests/value_arithmetic.c Normal file
View File

@@ -0,0 +1,126 @@
/**
* @file value_arithmetic.c
* @brief Tests the arithmetic operators, including the parts that look wrong.
*
* Where the reference does something odd, the assertion here pins the reference's
* answer, not the tidy one. A "fix" that changes any of these changes observable
* BASIC behaviour and belongs in its own commit with its own TODO.md entry.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/value.h>
#include "testutil.h"
static akbasic_Value A;
static akbasic_Value B;
static akbasic_Value SCRATCH;
static void set_int(akbasic_Value *v, int64_t n)
{
test_discard_error(akbasic_value_zero(v));
v->valuetype = AKBASIC_TYPE_INTEGER;
v->intval = n;
}
static void set_float(akbasic_Value *v, double n)
{
test_discard_error(akbasic_value_zero(v));
v->valuetype = AKBASIC_TYPE_FLOAT;
v->floatval = n;
}
static void set_string(akbasic_Value *v, const char *s)
{
test_discard_error(akbasic_value_zero(v));
v->valuetype = AKBASIC_TYPE_STRING;
strncpy(v->stringval, s, AKBASIC_MAX_STRING_LENGTH - 1);
}
int main(void)
{
akbasic_Value *out = NULL;
char rendered[AKBASIC_MAX_STRING_LENGTH];
TEST_REQUIRE_OK(akbasic_error_register());
/* Integer arithmetic. */
set_int(&A, 7);
set_int(&B, 3);
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 10);
TEST_REQUIRE_OK(akbasic_value_math_minus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 4);
TEST_REQUIRE_OK(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 21);
TEST_REQUIRE_OK(akbasic_value_math_divide(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 2); /* truncating, as C and Go both do */
/* Float arithmetic. */
set_float(&A, 1.20);
set_float(&B, 0.4);
TEST_REQUIRE_OK(akbasic_value_math_divide(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_OK(akbasic_value_to_string(out, rendered, sizeof(rendered)));
/* tests/language/arithmetic/float.txt says 3.000000, not 2.999999. */
TEST_REQUIRE_STR(rendered, "3.000000");
/* String concatenation, including the mixed-type forms. */
set_string(&A, "SORTED IN ");
set_int(&B, 4);
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_STR(out->stringval, "SORTED IN 4");
set_string(&A, "X=");
set_float(&B, 2.5);
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_STR(out->stringval, "X=2.500000");
/* String multiplication is repetition. */
set_string(&A, "ab");
set_int(&B, 3);
TEST_REQUIRE_OK(akbasic_value_math_multiply(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_STR(out->stringval, "ababab");
/*
* mathPlus mutates self in place when self is mutable, where every other
* operator always clones. CommandNEXT's loop increment depends on it --
* TODO.md section 12 item 4. Pinning the asymmetry so a "cleanup" fails here
* rather than silently breaking every FOR loop.
*/
set_int(&A, 10);
A.mutable_ = true;
set_int(&B, 5);
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE(out == &A, "math_plus on a mutable value must return self, not the scratch");
TEST_REQUIRE_INT(A.intval, 15);
set_int(&A, 10);
A.mutable_ = false;
TEST_REQUIRE_OK(akbasic_value_math_plus(&A, &B, &SCRATCH, &out));
TEST_REQUIRE(out == &SCRATCH, "math_plus on an immutable value must use the scratch");
TEST_REQUIRE_INT(A.intval, 10);
/* Type errors. */
set_string(&A, "text");
set_int(&B, 1);
TEST_REQUIRE_STATUS(akbasic_value_math_minus(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_math_divide(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_invert(&A, &SCRATCH, &out), AKBASIC_ERR_TYPE);
/* Division by zero raises rather than trapping or panicking. */
set_int(&A, 1);
set_int(&B, 0);
TEST_REQUIRE_STATUS(akbasic_value_math_divide(&A, &B, &SCRATCH, &out), AKBASIC_ERR_VALUE);
/* Unary minus. */
set_int(&A, 42);
TEST_REQUIRE_OK(akbasic_value_invert(&A, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, -42);
/* nil rval is rejected everywhere. */
TEST_REQUIRE_STATUS(akbasic_value_math_plus(&A, NULL, &SCRATCH, &out), AKERR_NULLPOINTER);
return akbasic_test_failures;
}

73
tests/value_bitwise.c Normal file
View File

@@ -0,0 +1,73 @@
/**
* @file value_bitwise.c
* @brief Tests the bitwise operators and their type guards.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/value.h>
#include "testutil.h"
static akbasic_Value A;
static akbasic_Value B;
static akbasic_Value SCRATCH;
static void set_int(akbasic_Value *v, int64_t n)
{
test_discard_error(akbasic_value_zero(v));
v->valuetype = AKBASIC_TYPE_INTEGER;
v->intval = n;
}
int main(void)
{
akbasic_Value *out = NULL;
TEST_REQUIRE_OK(akbasic_error_register());
set_int(&A, 0x0F);
set_int(&B, 0x33);
TEST_REQUIRE_OK(akbasic_value_bitwise_and(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 0x03);
TEST_REQUIRE_OK(akbasic_value_bitwise_or(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 0x3F);
TEST_REQUIRE_OK(akbasic_value_bitwise_xor(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 0x3C);
set_int(&A, 1);
TEST_REQUIRE_OK(akbasic_value_shift_left(&A, 8, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 256);
set_int(&A, 256);
TEST_REQUIRE_OK(akbasic_value_shift_right(&A, 8, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, 1);
set_int(&A, 0);
TEST_REQUIRE_OK(akbasic_value_bitwise_not(&A, &SCRATCH, &out));
TEST_REQUIRE_INT(out->intval, -1);
/*
* Go's shift is defined for any count; C's is not. Refusing an out-of-range
* count is a deliberate deviation from the reference, which would have
* produced a defined (if useless) answer where C would produce UB.
*/
set_int(&A, 1);
TEST_REQUIRE_STATUS(akbasic_value_shift_left(&A, 64, &SCRATCH, &out), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_value_shift_left(&A, -1, &SCRATCH, &out), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(akbasic_value_shift_right(&A, 64, &SCRATCH, &out), AKBASIC_ERR_VALUE);
/* Bitwise operations are integers-only. */
test_discard_error(akbasic_value_zero(&A));
A.valuetype = AKBASIC_TYPE_FLOAT;
A.floatval = 1.0;
set_int(&B, 1);
TEST_REQUIRE_STATUS(akbasic_value_bitwise_and(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_bitwise_or(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_bitwise_xor(&A, &B, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_bitwise_not(&A, &SCRATCH, &out), AKBASIC_ERR_TYPE);
TEST_REQUIRE_STATUS(akbasic_value_shift_left(&A, 1, &SCRATCH, &out), AKBASIC_ERR_TYPE);
return akbasic_test_failures;
}

102
tests/value_compare.c Normal file
View File

@@ -0,0 +1,102 @@
/**
* @file value_compare.c
* @brief Tests the comparison operators across integer, float and string.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/value.h>
#include "testutil.h"
static akbasic_Value A;
static akbasic_Value B;
static akbasic_Value SCRATCH;
static void set_int(akbasic_Value *v, int64_t n)
{
test_discard_error(akbasic_value_zero(v));
v->valuetype = AKBASIC_TYPE_INTEGER;
v->intval = n;
}
static void set_float(akbasic_Value *v, double n)
{
test_discard_error(akbasic_value_zero(v));
v->valuetype = AKBASIC_TYPE_FLOAT;
v->floatval = n;
}
static void set_string(akbasic_Value *v, const char *s)
{
test_discard_error(akbasic_value_zero(v));
v->valuetype = AKBASIC_TYPE_STRING;
strncpy(v->stringval, s, AKBASIC_MAX_STRING_LENGTH - 1);
}
#define EXPECT_BOOL(__call, __want) \
do { \
akbasic_Value *__out = NULL; \
TEST_REQUIRE_OK(__call(&A, &B, &SCRATCH, &__out)); \
TEST_REQUIRE(__out->valuetype == AKBASIC_TYPE_BOOLEAN, \
#__call " did not produce a boolean"); \
TEST_REQUIRE(akbasic_value_is_true(__out) == (__want), \
#__call " expected %s", ((__want) ? "true" : "false")); \
TEST_REQUIRE_INT(__out->boolvalue, ((__want) ? AKBASIC_TRUE : AKBASIC_FALSE)); \
} while ( 0 )
int main(void)
{
akbasic_Value *out = NULL;
TEST_REQUIRE_OK(akbasic_error_register());
/* Integers. */
set_int(&A, 3);
set_int(&B, 5);
EXPECT_BOOL(akbasic_value_less_than, true);
EXPECT_BOOL(akbasic_value_less_than_equal, true);
EXPECT_BOOL(akbasic_value_greater_than, false);
EXPECT_BOOL(akbasic_value_greater_than_equal, false);
EXPECT_BOOL(akbasic_value_is_equal, false);
EXPECT_BOOL(akbasic_value_is_not_equal, true);
set_int(&B, 3);
EXPECT_BOOL(akbasic_value_less_than, false);
EXPECT_BOOL(akbasic_value_less_than_equal, true);
EXPECT_BOOL(akbasic_value_greater_than_equal, true);
EXPECT_BOOL(akbasic_value_is_equal, true);
/* Floats. */
set_float(&A, 1.5);
set_float(&B, 2.5);
EXPECT_BOOL(akbasic_value_less_than, true);
EXPECT_BOOL(akbasic_value_greater_than, false);
/* Strings compare lexically. */
set_string(&A, "APPLE");
set_string(&B, "BANANA");
EXPECT_BOOL(akbasic_value_less_than, true);
EXPECT_BOOL(akbasic_value_greater_than, false);
EXPECT_BOOL(akbasic_value_is_equal, false);
set_string(&B, "APPLE");
EXPECT_BOOL(akbasic_value_is_equal, true);
EXPECT_BOOL(akbasic_value_less_than_equal, true);
EXPECT_BOOL(akbasic_value_greater_than_equal, true);
/* BASIC true is -1, not 1: the Commodore convention the README calls out. */
set_int(&A, 1);
set_int(&B, 1);
TEST_REQUIRE_OK(akbasic_value_is_equal(&A, &B, &SCRATCH, &out));
TEST_REQUIRE_INT(out->boolvalue, -1);
/* is_true is false for anything that is not a boolean. */
set_int(&A, -1);
TEST_REQUIRE(!akbasic_value_is_true(&A), "an integer -1 is not a BASIC true");
TEST_REQUIRE(!akbasic_value_is_true(NULL), "NULL is not true");
TEST_REQUIRE_STATUS(akbasic_value_is_equal(&A, NULL, &SCRATCH, &out), AKERR_NULLPOINTER);
return akbasic_test_failures;
}

123
tests/variable_subscript.c Normal file
View File

@@ -0,0 +1,123 @@
/**
* @file variable_subscript.c
* @brief Tests variable storage, typing by suffix, and subscript arithmetic.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/variable.h>
#include "testutil.h"
static akbasic_ValuePool POOL;
static akbasic_Variable VAR;
static void name_it(const char *name)
{
memset(&VAR, 0, sizeof(VAR));
strncpy(VAR.name, name, sizeof(VAR.name) - 1);
}
int main(void)
{
akbasic_Value *slot = NULL;
akerr_ErrorContext *e = NULL;
int64_t sizes1[1] = { 5 };
int64_t sizes2[2] = { 8, 8 };
int64_t sizes3[3] = { 2, 3, 4 };
int64_t at[3] = { 0, 0, 0 };
TEST_REQUIRE_OK(akbasic_error_register());
TEST_REQUIRE_OK(akbasic_valuepool_init(&POOL));
/* The suffix picks the type. Note % is float here, inverting Commodore. */
name_it("A#");
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes1, 1));
TEST_REQUIRE_INT(VAR.valuetype, AKBASIC_TYPE_INTEGER);
TEST_REQUIRE_INT(VAR.valuecount, 5);
name_it("A%");
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes1, 1));
TEST_REQUIRE_INT(VAR.valuetype, AKBASIC_TYPE_FLOAT);
name_it("A$");
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes1, 1));
TEST_REQUIRE_INT(VAR.valuetype, AKBASIC_TYPE_STRING);
/* One dimension: round-trip every element. */
name_it("B#");
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes1, 1));
for ( at[0] = 0; at[0] < 5; at[0]++ ) {
TEST_REQUIRE_OK(akbasic_variable_set_integer(&VAR, at[0] * 10, at, 1));
}
for ( at[0] = 0; at[0] < 5; at[0]++ ) {
TEST_REQUIRE_OK(akbasic_variable_get_subscript(&VAR, at, 1, &slot));
TEST_REQUIRE_INT(slot->intval, at[0] * 10);
}
/* Two dimensions, the shape array_multidimensional.bas uses. */
name_it("C#");
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes2, 2));
TEST_REQUIRE_INT(VAR.valuecount, 64);
at[0] = 0; at[1] = 7;
TEST_REQUIRE_OK(akbasic_variable_set_integer(&VAR, 31337, at, 2));
at[0] = 1; at[1] = 7;
TEST_REQUIRE_OK(akbasic_variable_set_integer(&VAR, 65535, at, 2));
at[0] = 0; at[1] = 7;
TEST_REQUIRE_OK(akbasic_variable_get_subscript(&VAR, at, 2, &slot));
TEST_REQUIRE_INT(slot->intval, 31337);
at[0] = 1; at[1] = 7;
TEST_REQUIRE_OK(akbasic_variable_get_subscript(&VAR, at, 2, &slot));
TEST_REQUIRE_INT(slot->intval, 65535);
/* Three dimensions, to prove the flattening multiplier walks correctly. */
name_it("D#");
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes3, 3));
TEST_REQUIRE_INT(VAR.valuecount, 24);
at[0] = 1; at[1] = 2; at[2] = 3;
TEST_REQUIRE_OK(akbasic_variable_set_integer(&VAR, 99, at, 3));
TEST_REQUIRE_OK(akbasic_variable_get_subscript(&VAR, at, 3, &slot));
TEST_REQUIRE_INT(slot->intval, 99);
at[0] = 0; at[1] = 0; at[2] = 0;
TEST_REQUIRE_OK(akbasic_variable_get_subscript(&VAR, at, 3, &slot));
TEST_REQUIRE_INT(slot->intval, 0);
/*
* The out-of-bounds message is compared with strcmp, not merely for status:
* tests/language/array_outofbounds.txt reproduces it verbatim, so a reworded
* message is a failing golden case.
*/
name_it("A#");
sizes1[0] = 3;
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes1, 1));
at[0] = 4;
e = akbasic_variable_get_subscript(&VAR, at, 1, &slot);
TEST_REQUIRE(e != NULL, "an out-of-bounds subscript must raise");
if ( e != NULL ) {
TEST_REQUIRE_INT(e->status, AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STR(e->message,
"Variable index access out of bounds at dimension 0: 4 (max 2)");
test_discard_error(e);
}
/* Wrong subscript count. */
name_it("E#");
TEST_REQUIRE_OK(akbasic_variable_init(&VAR, &POOL, sizes2, 2));
at[0] = 0;
TEST_REQUIRE_STATUS(akbasic_variable_get_subscript(&VAR, at, 1, &slot), AKBASIC_ERR_BOUNDS);
/* A dimension must be positive. */
name_it("F#");
sizes1[0] = 0;
TEST_REQUIRE_STATUS(akbasic_variable_init(&VAR, &POOL, sizes1, 1), AKBASIC_ERR_VALUE);
sizes1[0] = -1;
TEST_REQUIRE_STATUS(akbasic_variable_init(&VAR, &POOL, sizes1, 1), AKBASIC_ERR_VALUE);
/* An unnamed variable has no type to derive. */
memset(&VAR, 0, sizeof(VAR));
sizes1[0] = 1;
TEST_REQUIRE_STATUS(akbasic_variable_init(&VAR, &POOL, sizes1, 1), AKBASIC_ERR_VALUE);
return akbasic_test_failures;
}

91
tests/verbs_table.c Normal file
View File

@@ -0,0 +1,91 @@
/**
* @file verbs_table.c
* @brief Tests the dispatch table's invariants.
*
* The sorting assertion is the important one: bsearch on a mis-sorted table
* silently fails to find a verb rather than producing a compile error, and the
* symptom would be "Unknown command PRINT" a long way from the cause.
*/
#include <string.h>
#include <akbasic/error.h>
#include <akbasic/verbs.h>
#include "testutil.h"
int main(void)
{
const akbasic_Verb *table = NULL;
const akbasic_Verb *found = NULL;
int count = 0;
int i = 0;
TEST_REQUIRE_OK(akbasic_error_register());
table = akbasic_verb_table(&count);
TEST_REQUIRE(table != NULL, "the verb table must exist");
TEST_REQUIRE(count > 0, "the verb table must not be empty");
/* Sorted, strictly: bsearch requires it and a duplicate name is a bug. */
for ( i = 1; i < count; i++ ) {
TEST_REQUIRE(strcmp(table[i - 1].name, table[i].name) < 0,
"table is not sorted at index %d: \"%s\" then \"%s\"",
i, table[i - 1].name, table[i].name);
}
/* Every row is reachable by its own name. */
for ( i = 0; i < count; i++ ) {
TEST_REQUIRE_OK(akbasic_verb_lookup(table[i].name, &found));
TEST_REQUIRE(found == &table[i], "row \"%s\" is not findable", table[i].name);
}
/* Verbs and function names are case-insensitive; variable names are not. */
TEST_REQUIRE_OK(akbasic_verb_lookup("print", &found));
TEST_REQUIRE(found != NULL && strcmp(found->name, "PRINT") == 0, "lowercase print missed");
TEST_REQUIRE_OK(akbasic_verb_lookup("PrInT", &found));
TEST_REQUIRE(found != NULL && strcmp(found->name, "PRINT") == 0, "mixed-case print missed");
/* A miss is a NULL, not an error. */
TEST_REQUIRE_OK(akbasic_verb_lookup("NOTAVERB", &found));
TEST_REQUIRE(found == NULL, "an unknown name must miss");
TEST_REQUIRE_OK(akbasic_verb_lookup("", &found));
TEST_REQUIRE(found == NULL, "an empty name must miss");
/* Every function row carries a real arity and a handler. */
for ( i = 0; i < count; i++ ) {
if ( table[i].tokentype != AKBASIC_TOK_FUNCTION ) {
continue;
}
TEST_REQUIRE(table[i].arity >= 1,
"function \"%s\" has arity %d", table[i].name, table[i].arity);
TEST_REQUIRE(table[i].exec != NULL,
"function \"%s\" has no exec handler", table[i].name);
}
/*
* The four rows with no exec handler are consumed by another verb's parse
* path and are never evaluated on their own. Any other missing handler is a
* verb that would report "Unknown command" at runtime.
*/
for ( i = 0; i < count; i++ ) {
if ( table[i].exec != NULL ) {
continue;
}
TEST_REQUIRE(strcmp(table[i].name, "AND") == 0 ||
strcmp(table[i].name, "ELSE") == 0 ||
strcmp(table[i].name, "NOT") == 0 ||
strcmp(table[i].name, "OR") == 0 ||
strcmp(table[i].name, "REM") == 0 ||
strcmp(table[i].name, "STEP") == 0 ||
strcmp(table[i].name, "THEN") == 0 ||
strcmp(table[i].name, "TO") == 0,
"verb \"%s\" has no exec handler and is not a known passive token",
table[i].name);
}
TEST_REQUIRE_STATUS(akbasic_verb_lookup(NULL, &found), AKERR_NULLPOINTER);
TEST_REQUIRE_STATUS(akbasic_verb_lookup("PRINT", NULL), AKERR_NULLPOINTER);
return akbasic_test_failures;
}

58
tests/version_check.c Normal file
View File

@@ -0,0 +1,58 @@
/**
* @file version_check.c
* @brief Tests that the loaded dependencies match the headers we compiled against.
*
* The soname normally catches a mismatch at load time. This earns its keep when
* the soname is bypassed -- a 0.2.0 dropped in under the 0.1 filename loads
* happily, and only the check notices.
*/
#include <string.h>
#include <akstdlib.h>
#include <akbasic/error.h>
#include "testutil.h"
int main(void)
{
int major = 0;
int minor = 0;
int patch = 0;
TEST_REQUIRE_OK(akbasic_error_register());
/*
* AKSL_VERSION_CHECK() is a macro on purpose: it expands here so it captures
* the numbers *this* translation unit was built with, and compares them to
* the ones baked into the library that actually loaded.
*/
TEST_REQUIRE_OK(AKSL_VERSION_CHECK());
TEST_REQUIRE_OK(aksl_version(&major, &minor, &patch));
TEST_REQUIRE_INT(major, AKSL_VERSION_MAJOR);
TEST_REQUIRE_INT(minor, AKSL_VERSION_MINOR);
TEST_REQUIRE_STR(aksl_version_string(), AKSL_VERSION_STRING);
TEST_REQUIRE_STR(aksl_version_soname(), AKSL_VERSION_SONAME);
/*
* While libakstdlib's major is 0 the soname carries MAJOR.MINOR, so 0.1 and
* 0.2 are different ABIs. Asserting the shape here means a future 1.0 bump
* that forgets to change the soname rule fails a test rather than shipping.
*/
if ( major == 0 ) {
char expected[32];
snprintf(expected, sizeof(expected), "%d.%d", major, minor);
TEST_REQUIRE_STR(aksl_version_soname(), expected);
}
/*
* libakerror publishes no version macro at all, so there is nothing to
* compare. Its floor is the #error on AKERR_FIRST_CONSUMER_STATUS that
* akbasic/error.h carries -- if this file compiled, that guard passed.
*/
TEST_REQUIRE_INT(AKERR_FIRST_CONSUMER_STATUS, 256);
return akbasic_test_failures;
}