274 lines
9.3 KiB
C
274 lines
9.3 KiB
C
|
|
/**
|
||
|
|
* @file format_verbs.c
|
||
|
|
* @brief Tests the group D verbs: PRINT USING, PUDEF, WIDTH and CHAR.
|
||
|
|
*
|
||
|
|
* Field formatting has more edge cases than a verb usually does, and
|
||
|
|
* akbasic_format_using() is a pure function of a format string and a value, so
|
||
|
|
* most of this exercises it directly rather than through a program. What goes
|
||
|
|
* through a program is the part a program can get wrong: the parse of
|
||
|
|
* `PRINT USING fmt; value`, and the verbs that only set state.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/format.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#include "harness.h"
|
||
|
|
#include "testutil.h"
|
||
|
|
|
||
|
|
static akbasic_FormatState FORMAT;
|
||
|
|
|
||
|
|
/** @brief Run a program to completion in RUN mode, from a string. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
PASS(errctx, harness_start(NULL));
|
||
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
||
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Format one value, for the direct tests below. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *fmt_number(const char *format, double number, char *dest, size_t len)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_Value value;
|
||
|
|
|
||
|
|
PASS(errctx, akbasic_value_zero(&value));
|
||
|
|
value.valuetype = AKBASIC_TYPE_FLOAT;
|
||
|
|
value.floatval = number;
|
||
|
|
PASS(errctx, akbasic_format_using(&FORMAT, format, &value, dest, len));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Format one string, for the direct tests below. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *fmt_string(const char *format, const char *text, char *dest, size_t len)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_Value value;
|
||
|
|
|
||
|
|
PASS(errctx, akbasic_value_zero(&value));
|
||
|
|
value.valuetype = AKBASIC_TYPE_STRING;
|
||
|
|
snprintf(value.stringval, sizeof(value.stringval), "%s", text);
|
||
|
|
PASS(errctx, akbasic_format_using(&FORMAT, format, &value, dest, len));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Digit positions right-justify, and the decimal places are fixed. */
|
||
|
|
static void test_numeric_fields(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(fmt_number("###.##", 3.14159, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " 3.14");
|
||
|
|
|
||
|
|
/* Rounded to the field, not truncated. */
|
||
|
|
TEST_REQUIRE_OK(fmt_number("###.#", 2.06, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " 2.1");
|
||
|
|
|
||
|
|
/* No decimal point in the field means no fractional part printed. */
|
||
|
|
TEST_REQUIRE_OK(fmt_number("####", 42.9, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " 43");
|
||
|
|
|
||
|
|
/* Zero fills the whole field with blanks but the one digit. */
|
||
|
|
TEST_REQUIRE_OK(fmt_number("####", 0.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " 0");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Literal text around the field is copied through. */
|
||
|
|
static void test_literal_text(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_OK(fmt_number("TOTAL: ###.## EACH", 12.5, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "TOTAL: 12.50 EACH");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A comma in the field groups the integer part in threes. */
|
||
|
|
static void test_grouping(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_OK(fmt_number("#,###,###", 1234567.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "1,234,567");
|
||
|
|
|
||
|
|
/* Padding counts the separators, so a column stays a column. */
|
||
|
|
TEST_REQUIRE_OK(fmt_number("#,###,###", 12.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " 12");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A currency sign and a sign position are part of the field. */
|
||
|
|
static void test_decorations(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_OK(fmt_number("$#,###.##", 1234.5, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "$1,234.50");
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(fmt_number("+####", 42.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "+ 42");
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(fmt_number("+####", -42.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "- 42");
|
||
|
|
|
||
|
|
/* A trailing sign position puts the sign after the digits. */
|
||
|
|
TEST_REQUIRE_OK(fmt_number("####-", -42.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " 42-");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A value too wide for its field fills the field with stars.
|
||
|
|
*
|
||
|
|
* BASIC 7.0's answer, and deliberately loud: printing more digits than the field
|
||
|
|
* asked for would push every later column out of line, which is exactly what
|
||
|
|
* PRINT USING is for avoiding.
|
||
|
|
*/
|
||
|
|
static void test_overflow(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_OK(fmt_number("###", 99999.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "***");
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A negative number in a field with no sign position overflows too. Dropping
|
||
|
|
* the minus would print -5 as 5, which is worse than a row of stars.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_OK(fmt_number("###", -5.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "***");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief `=` centres a string in its field and `>` right-justifies it. */
|
||
|
|
static void test_string_fields(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_OK(fmt_string("==========", "MID", out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " MID ");
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(fmt_string(">>>>>>>>>>", "RIGHT", out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, " RIGHT");
|
||
|
|
|
||
|
|
/* A string longer than its field is cut, not starred. */
|
||
|
|
TEST_REQUIRE_OK(fmt_string("===", "TOOLONG", out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "TOO");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief A field and a value of the wrong kind for each other are refused. */
|
||
|
|
static void test_type_mismatch(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_STATUS(fmt_string("###", "TEXT", out, sizeof(out)), AKBASIC_ERR_TYPE);
|
||
|
|
TEST_REQUIRE_STATUS(fmt_number("===", 1.0, out, sizeof(out)), AKBASIC_ERR_TYPE);
|
||
|
|
|
||
|
|
/* A format with no field at all is a mistake rather than a literal. */
|
||
|
|
TEST_REQUIRE_STATUS(fmt_number("NO FIELD HERE", 1.0, out, sizeof(out)), AKBASIC_ERR_SYNTAX);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief PUDEF replaces the fill characters, as many as it was given. */
|
||
|
|
static void test_pudef(void)
|
||
|
|
{
|
||
|
|
char out[256];
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_pudef(&FORMAT, "*"));
|
||
|
|
TEST_REQUIRE_OK(fmt_number("#####", 42.0, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "***42");
|
||
|
|
|
||
|
|
/* The other three are untouched by a one-character PUDEF. */
|
||
|
|
TEST_REQUIRE_OK(fmt_number("#,###.##", 1234.5, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, "1,234.50");
|
||
|
|
|
||
|
|
/* All four at once. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_state_init(&FORMAT));
|
||
|
|
TEST_REQUIRE_OK(akbasic_format_pudef(&FORMAT, "_. ,"));
|
||
|
|
TEST_REQUIRE_OK(fmt_number("$#,###.##", 1234.5, out, sizeof(out)));
|
||
|
|
TEST_REQUIRE_STR(out, ",1.234 50");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief PRINT USING parses and prints through a running program. */
|
||
|
|
static void test_print_using(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 PRINT USING \"###.##\"; 3.14159\n"
|
||
|
|
"20 PRINT USING \"TOTAL: $#,###.##\"; 1234.5\n"
|
||
|
|
"30 PRINT USING \"###\"; 99999\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, " 3.14\nTOTAL: $1,234.50\n***\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* A plain PRINT still works, which the shared parse handler could break. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 PRINT \"PLAIN\"\n20 PRINT 1 + 2\n30 PRINT\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "PLAIN\n3\n\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* And PUDEF reaches PRINT USING through the runtime's own state. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 PUDEF \"*\"\n"
|
||
|
|
"20 PRINT USING \"#####\"; 42\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "***42\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* The semicolon is required; without it the format has no value to print. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 PRINT USING \"###\" 42\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "semicolon") != NULL,
|
||
|
|
"a missing semicolon should say so, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief WIDTH takes 1 or 2 and refuses anything else. */
|
||
|
|
static void test_width(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 WIDTH 2\n20 PRINT \"OK\"\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "OK\n");
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.gfx.linewidth, 2);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 WIDTH 3\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "WIDTH is 1 or 2") != NULL,
|
||
|
|
"WIDTH 3 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* No device needed: it is drawing state, like COLOR's bindings. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 WIDTH 2\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "device") == NULL,
|
||
|
|
"WIDTH should not need a device, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief CHAR refuses against a sink with no cursor, which is the stdio one. */
|
||
|
|
static void test_char_needs_a_cursor(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 CHAR 1, 5, 3, \"HI\"\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "position its cursor") != NULL,
|
||
|
|
"CHAR against a stdio sink should refuse by name, got \"%s\"",
|
||
|
|
HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
test_numeric_fields();
|
||
|
|
test_literal_text();
|
||
|
|
test_grouping();
|
||
|
|
test_decorations();
|
||
|
|
test_overflow();
|
||
|
|
test_string_fields();
|
||
|
|
test_type_mismatch();
|
||
|
|
test_pudef();
|
||
|
|
test_print_using();
|
||
|
|
test_width();
|
||
|
|
test_char_needs_a_cursor();
|
||
|
|
return akbasic_test_failures;
|
||
|
|
}
|