281 lines
9.1 KiB
C
281 lines
9.1 KiB
C
|
|
/**
|
||
|
|
* @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 <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#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");
|
||
|
|
}
|
||
|
|
|
||
|
|
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();
|
||
|
|
return akbasic_test_failures;
|
||
|
|
}
|