/** * @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 #include #include #include #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; }