/** * @file disk_verbs.c * @brief Tests the group F verbs: channels, files, and the ones that are refused. * * These touch the filesystem, so they work in a temporary directory of their own * and clean up after themselves. What is asserted is the seam between BASIC and * `aksl_f*` -- that a verb reaches the right call with the right arguments -- * plus, for the five that need a real 1541, that they refuse *by name* rather * than doing something plausible and wrong. */ #include #include #include #include #include #include #include "harness.h" #include "testutil.h" /** @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 Read a whole file, for checking what a verb wrote. */ static bool file_contents(const char *name, char *dest, size_t len) { FILE *fp = fopen(name, "rb"); size_t got = 0; if ( fp == NULL ) { return false; } got = fread(dest, 1, len - 1, fp); dest[got] = '\0'; fclose(fp); return true; } /** @brief A channel written and then read back gives the same lines. */ static void test_channel_round_trip(void) { char contents[256]; remove("t_chan.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_chan.txt\", W\n" "20 PRINT #1, \"FIRST\"\n" "30 PRINT #1, \"SECOND\"\n" "40 DCLOSE 1\n" "50 DOPEN 2, \"t_chan.txt\"\n" "60 INPUT #2, A$\n" "70 INPUT #2, B$\n" "80 DCLOSE 2\n" "90 PRINT A$\n" "100 PRINT B$\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "FIRST\nSECOND\n"); TEST_REQUIRE(file_contents("t_chan.txt", contents, sizeof(contents)), "the file should exist"); TEST_REQUIRE_STR(contents, "FIRST\nSECOND\n"); harness_stop(); remove("t_chan.txt"); } /** @brief A numeric variable reads a numeric line, and end of file gives zero. */ static void test_channel_numeric(void) { remove("t_num.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_num.txt\", W\n" "20 PRINT #1, 42\n" "30 DCLOSE 1\n" "40 DOPEN 2, \"t_num.txt\"\n" "50 INPUT #2, A#\n" "60 INPUT #2, B#\n" "70 DCLOSE 2\n" "80 PRINT A#\n" "90 PRINT B#\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "42\n0\n"); harness_stop(); remove("t_num.txt"); } /** @brief APPEND adds to a file rather than replacing it. */ static void test_append(void) { char contents[256]; remove("t_app.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_app.txt\", W\n" "20 PRINT #1, \"ONE\"\n" "30 DCLOSE 1\n" "40 APPEND 1, \"t_app.txt\"\n" "50 PRINT #1, \"TWO\"\n" "60 DCLOSE 1\n")); TEST_REQUIRE(file_contents("t_app.txt", contents, sizeof(contents)), "the file should exist"); TEST_REQUIRE_STR(contents, "ONE\nTWO\n"); harness_stop(); remove("t_app.txt"); } /** @brief Reading a channel opened for writing is refused, and the reverse too. */ static void test_channel_direction(void) { remove("t_dir.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_dir.txt\", W\n" "20 INPUT #1, A$\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "opened for writing") != NULL, "reading a write channel should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_dir.txt\"\n" "20 PRINT #1, \"NO\"\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "opened for reading") != NULL, "writing a read channel should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); remove("t_dir.txt"); } /** @brief An unopened or already-open channel is reported rather than silently reused. */ static void test_channel_state(void) { TEST_REQUIRE_OK(run_program("10 PRINT #3, \"NOWHERE\"\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is not open") != NULL, "an unopened channel should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); remove("t_two.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_two.txt\", W\n" "20 DOPEN 1, \"t_two.txt\", W\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "already open") != NULL, "re-opening a channel should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); remove("t_two.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 99, \"x\"\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..9") != NULL, "channel 99 should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief COPY duplicates a file and CONCAT appends one to another. */ static void test_copy_and_concat(void) { char contents[256]; remove("t_a.txt"); remove("t_b.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_a.txt\", W\n" "20 PRINT #1, \"AAA\"\n" "30 DCLOSE 1\n" "40 COPY \"t_a.txt\", \"t_b.txt\"\n" "50 CONCAT \"t_a.txt\", \"t_b.txt\"\n")); TEST_REQUIRE(file_contents("t_b.txt", contents, sizeof(contents)), "the copy should exist"); TEST_REQUIRE_STR(contents, "AAA\nAAA\n"); harness_stop(); remove("t_a.txt"); remove("t_b.txt"); } /** @brief RENAME moves a file and SCRATCH deletes one. */ static void test_rename_and_scratch(void) { char contents[256]; remove("t_from.txt"); remove("t_to.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_from.txt\", W\n" "20 PRINT #1, \"MOVED\"\n" "30 DCLOSE 1\n" "40 RENAME \"t_from.txt\", \"t_to.txt\"\n")); TEST_REQUIRE(!file_contents("t_from.txt", contents, sizeof(contents)), "the original should be gone"); TEST_REQUIRE(file_contents("t_to.txt", contents, sizeof(contents)), "the new name should exist"); harness_stop(); TEST_REQUIRE_OK(run_program("10 SCRATCH \"t_to.txt\"\n")); TEST_REQUIRE(!file_contents("t_to.txt", contents, sizeof(contents)), "SCRATCH should have deleted it"); harness_stop(); /* Deleting something that is not there is reported rather than ignored. */ TEST_REQUIRE_OK(run_program("10 SCRATCH \"t_nosuch.txt\"\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "could not delete") != NULL, "deleting a missing file should be reported, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief BSAVE writes a memory range and BLOAD reads one back. */ static void test_bsave_bload(void) { remove("t_bin.dat"); TEST_REQUIRE_OK(run_program("10 A$ = \"BINARY\"\n" "20 BSAVE \"t_bin.dat\", POINTER(A$), POINTER(A$) + 7\n" "30 B$ = \".......\"\n" "40 BLOAD \"t_bin.dat\", POINTER(B$), 7\n" "50 PRINT B$\n")); TEST_REQUIRE_STR(HARNESS_OUTPUT, "BINARY\n"); harness_stop(); remove("t_bin.dat"); /* BLOAD needs a length, unlike a C128's; see TODO.md section 5. */ TEST_REQUIRE_OK(run_program("10 A$ = \"X\"\n20 BLOAD \"t_bin.dat\", POINTER(A$)\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "maximum length") != NULL, "BLOAD without a length should be refused, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** * @brief The five verbs that need a real drive refuse by name, with the reason. * * The point is that they do not do something plausible instead. `HEADER` on a * filesystem would have to mean "delete everything here", which is a * spectacularly bad thing to do to somebody who typed a C128 verb. */ static void test_no_drive_verbs(void) { struct { const char *program; const char *expect; } cases[] = { { "10 HEADER \"DISK\"\n", "formats a disk" }, { "10 COLLECT\n", "block allocation map" }, { "10 BACKUP\n", "duplicates one disk" }, { "10 BOOT\n", "boot sector" }, }; size_t i = 0; for ( i = 0; i < sizeof(cases) / sizeof(cases[0]); i++ ) { TEST_REQUIRE_OK(run_program(cases[i].program)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, cases[i].expect) != NULL, "expected \"%s\" in the refusal, got \"%s\"", cases[i].expect, HARNESS_OUTPUT); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "no disk drive") != NULL, "the refusal should say why, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /* DIRECTORY is refused for a different reason, and says which. */ TEST_REQUIRE_OK(run_program("10 DIRECTORY\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") != NULL, "DIRECTORY should name the missing wrapper, got \"%s\"", HARNESS_OUTPUT); harness_stop(); } /** @brief DCLEAR closes every channel, which is the half of it that means something. */ static void test_dclear(void) { remove("t_clear.txt"); TEST_REQUIRE_OK(run_program("10 DOPEN 1, \"t_clear.txt\", W\n" "20 DCLEAR\n" "30 PRINT #1, \"NO\"\n")); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is not open") != NULL, "DCLEAR should have closed the channel, got \"%s\"", HARNESS_OUTPUT); harness_stop(); remove("t_clear.txt"); } /** * @brief DLOAD from a prompt loads the whole program and nothing else. * * It used to lose the last line. DLOAD scans the file through the runtime's own * scanner, so by the time it returns, the tokens of the line that ran it are * gone and `hadlinenumber` and `environment->lineno` describe the last line of * the *file*. The REPL then carried on parsing the buffer it thought was still * its own, decided the leftovers were program text because hadlinenumber was * now true, and filed the `DLOAD` command itself under that line number -- * silently replacing the last line of every program loaded from a prompt. * * Found while making the examples in docs/ execute: a save/load round trip is * the second thing chapter 2 shows a reader. */ static akerr_ErrorContext AKERR_NOIGNORE *test_dload_keeps_the_last_line(void) { PREPARE_ERROR(errctx); remove("t_round.bas"); TEST_REQUIRE_OK(harness_start("10 PRINT \"FIRST\"\n" "20 PRINT \"SECOND\"\n" "30 PRINT \"THIRD\"\n" "DSAVE \"t_round.bas\"\n" "NEW\n" "DLOAD \"t_round.bas\"\n" "LIST\n" "RUN\n")); PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_REPL)); PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 200)); TEST_REQUIRE(strstr(HARNESS_OUTPUT, "FIRST\nSECOND\nTHIRD\n") != NULL, "a save/load round trip should run all three lines, got \"%s\"", HARNESS_OUTPUT); /* * And the command is not in the program. LIST rather than the run output, * because a clobbered last line reloads the file and re-runs it, which * prints the right text for the wrong reason. */ TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DLOAD") == NULL, "the DLOAD command should not have been filed as program text, got \"%s\"", HARNESS_OUTPUT); harness_stop(); remove("t_round.bas"); SUCCEED_RETURN(errctx); } int main(void) { test_channel_round_trip(); test_channel_numeric(); test_append(); test_channel_direction(); test_channel_state(); test_copy_and_concat(); test_rename_and_scratch(); test_bsave_bload(); test_no_drive_verbs(); test_dclear(); IGNORE(test_dload_keeps_the_last_line()); return akbasic_test_failures; }