* tests: split of symtable testing into its own suite makes sense to be there, not in the lisp API * tests: Added string view suite sv_copy is the only function, but we may have others later. * tests: Meaningful and pretty logging for tests * tests: slight cleanliness * tests: c23 allows you to inline stack allocated arrays in struct decls * test: Added definition to make default testing less verbose TEST_VERBOSE is a preprocesser directive which TEST is dependent on. By default it is 0, in which case TEST simply fails if the condition is not true. Otherwise, a full log (as done previously) is made. * Makefile: added mode flag for full logs MODE=full will initialise a debug build with all logs, including test logs. Otherwise, MODE=debug just sets up standard debug build with main logs but no testing logs. MODE=release optimises and strips all logs. * tests: fix size of LISP_API_SUITE tests * test_lisp_api: int_test -> smi_test, added smi_oob_test * test_lisp_api: sym_test -> sym_fresh_test * test_lisp_api: added sym_unique_test * alisp.org: Added some tasks * symtable: sym_table_cleanup -> sym_table_free * lisp: split off lisp_free as it's own function lisp_free will do a shallow clean of any object, freeing its associated memory. It won't recur through any containers, nor will it freakout if you give it something that is constant (symbols, small integers, NIL, etc). * test_lisp_api: added sys_test * test_stream: basic skeleton * test_stream: implement stream_test_string * test_stream: Enable only stream_test_string * tests: enable STREAM_SUITE * sv: fix possible runtime issue with NULL SV's in sv_copy * alisp.org: add TODOs for all the tests required for streams * tests: Better suite creation While the previous method of in-lining a stack allocated array of tests into the suite struct declaration was nice, we had to update size manually. This macro will allow us to just append new tests to the suite without having to care for that. It generates a uniquely named variable for the test array, then uses that test array in the suite declaration. Nice and easy. * test: TEST_INIT macro as a prologue for any unit test * main: Put all variable declarations at start of main to ensure decl There is a chance that /end/ is jumped to without the FILE pointer or stream actually being declared. This deals with that. * stream: Make stream name a constant cstr We don't deal with the memory for it anyway. * stream: do not initialise file streams with a non-empty vector Because of the not_inlined trick, a 0 initialised SBO vector is completely valid to use if required. Future /vec_ensure/'s will deal with it appropriately. So there's no need to initialise the vector ahead of time like this. * test_stream: implement stream_test_file We might need to setup a prelude for initialising a file in the filesystem for testing here - not only does stream_test_file need it, but I see later tests requiring an equivalence check for files and strings (variants of a stream). * test_stream: setup prologue and epilogue as fake tests in the suite Standard old test functions, but they don't call TEST_INIT or TEST_PASSED. They're placed at the start and at the end of the test array. Those macros just do printing anyway, so they're not necessary. * tests: TEST_INIT -> TEST_START, TEST_PASSED -> TEST_END * tests: TEST_START only logs if TEST_VERBOSE is enabled. * test_lisp_api: "cons'" -> "conses" * alisp.org: Mark off completed stream_test_file * test_stream: implement stream_test_peek_next * test_stream: randomise filename Just to make sure it's not hardcoded or anything. * test_stream: make filename bigger, and increase the random alphabet * test: seed random number generator * test_stream: don't write null terminator to mock file * stream: stream_seek will do clamped movement if offset is invalid If a forward/backward offset is too big, we'll clamp to the edges of the file rather than failing completely. We return the number of bytes moved so callers can still validate, but the stream API can now deal with these situations a bit more effectively. * test_stream: implement stream_test_seek * stream: ensure stream_stop resets the FILE pointer if STREAM_TYPE_FILE * stream: stream_substr's call to stream_seek_forward refactored Following stream_seek_forward's own refactor, where we get offsets back instead of just a boolean, we should verify that offset. * main: put stream_stop before FILE pointer close As stream_stop requires a valid FILE pointer (fseek), we need to do it before we close the pipe. * test_vec: vec_test_substr -> vec_test_gen_substr * test_stream: implement stream_test_substr * alisp: add TODO for sv_t
425 lines
13 KiB
C
425 lines
13 KiB
C
/* test_stream.c: Stream tests
|
|
* Created: 2026-02-05
|
|
* Author: Aryadev Chavali
|
|
* License: See end of file
|
|
* Commentary:
|
|
*/
|
|
|
|
#include <malloc.h>
|
|
#include <stdio.h>
|
|
|
|
#include "./data.h"
|
|
#include "./test.h"
|
|
|
|
#include <alisp/stream.h>
|
|
#include <string.h>
|
|
|
|
char valid_filename[50];
|
|
FILE *valid_fp = NULL;
|
|
FILE *invalid_fp = NULL;
|
|
|
|
void stream_test_prologue(void)
|
|
{
|
|
const char filename_prefix[] = "build/stream_test_";
|
|
valid_filename[ARRSIZE(valid_filename) - 1] = '\0';
|
|
memcpy(valid_filename, filename_prefix, ARRSIZE(filename_prefix) - 1);
|
|
for (u64 i = ARRSIZE(filename_prefix) - 1; i < ARRSIZE(valid_filename) - 1;
|
|
++i)
|
|
{
|
|
u8 num = (rand() % 36);
|
|
if (num < 26)
|
|
{
|
|
valid_filename[i] = num + 'a';
|
|
}
|
|
else
|
|
{
|
|
valid_filename[i] = num + '0';
|
|
}
|
|
}
|
|
|
|
TEST_INFO("Creating file named `%.*s`\n", (int)ARRSIZE(valid_filename),
|
|
valid_filename);
|
|
valid_fp = fopen(valid_filename, "wb");
|
|
// This should do a few things for us
|
|
// 1) Create a file, or clear the contents of it if it exists already.
|
|
// 2) Write some content to it.
|
|
assert(valid_fp);
|
|
fwrite(words_text, ARRSIZE(words_text) - 1, 1, valid_fp);
|
|
fclose(valid_fp);
|
|
valid_fp = fopen(valid_filename, "rb");
|
|
assert(valid_fp);
|
|
|
|
invalid_fp = NULL;
|
|
}
|
|
|
|
void stream_test_epilogue(void)
|
|
{
|
|
TEST_INFO("Freeing resources and deleting file `%s`\n", valid_filename);
|
|
assert(valid_fp);
|
|
fclose(valid_fp);
|
|
remove(valid_filename);
|
|
}
|
|
|
|
void stream_test_string(void)
|
|
{
|
|
TEST_START();
|
|
sv_t test_strings[] = {
|
|
SV("hello, world!", 13),
|
|
SV("another string", 14),
|
|
SV((char *)text, ARRSIZE(text) / 2),
|
|
};
|
|
|
|
for (u64 i = 0; i < ARRSIZE(test_strings); ++i)
|
|
{
|
|
sv_t copy = sv_copy(test_strings[i]);
|
|
|
|
stream_t stream = {0};
|
|
stream_err_t err = stream_init_string(&stream, NULL, test_strings[i]);
|
|
TEST(err == STREAM_ERR_OK, "Stream initialising did not fail: %s",
|
|
stream_err_to_cstr(err));
|
|
TEST(stream_size(&stream) == test_strings[i].size,
|
|
"Stream size is always string size (%lu == %lu)", stream_size(&stream),
|
|
test_strings[i].size);
|
|
TEST(!stream_eoc(&stream), "Not end of content already");
|
|
|
|
stream_stop(&stream);
|
|
TEST(strncmp(copy.data, test_strings[i].data, copy.size) == 0,
|
|
"Freeing a stream does not free the underlying memory it was derived "
|
|
"from");
|
|
|
|
free(copy.data);
|
|
}
|
|
|
|
stream_t stream = {0};
|
|
stream_err_t err = stream_init_string(&stream, NULL, SV(NULL, 0));
|
|
TEST(err == STREAM_ERR_OK, "NULL stream initialising did not fail: %s",
|
|
stream_err_to_cstr(err));
|
|
TEST(stream_size(&stream) == 0, "NULL stream size is 0");
|
|
TEST(stream_eoc(&stream), "NULL stream is always at end of content");
|
|
stream_stop(&stream);
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
void stream_test_file(void)
|
|
{
|
|
TEST_START();
|
|
|
|
// Test that initialising works correctly
|
|
{
|
|
stream_t stream = {0};
|
|
{
|
|
stream_err_t err = stream_init_file(&stream, valid_filename, valid_fp);
|
|
TEST(err == STREAM_ERR_OK, "Expected initialisating to be okay: %s",
|
|
stream_err_to_cstr(err));
|
|
}
|
|
TEST(stream_size(&stream) == 0, "Stream doesn't read on init: size = %lu",
|
|
stream_size(&stream));
|
|
TEST(!stream_eoc(&stream), "Stream should not be at the EoC from init.");
|
|
}
|
|
|
|
// try to initialise the stream again but against a nonexistent file - we're
|
|
// expecting an error.
|
|
{
|
|
stream_t stream = {0};
|
|
{
|
|
stream_err_t err = stream_init_file(&stream, NULL, invalid_fp);
|
|
TEST(err != STREAM_ERR_OK, "Expected initialisating to not be okay: %s",
|
|
stream_err_to_cstr(err));
|
|
}
|
|
}
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
void stream_test_peek_next(void)
|
|
{
|
|
TEST_START();
|
|
|
|
// Valid streams
|
|
{
|
|
stream_t stream = {0};
|
|
stream_init_file(&stream, valid_filename, valid_fp);
|
|
|
|
u64 old_position = stream.position;
|
|
char c1 = stream_peek(&stream);
|
|
TEST(c1 != '\0', "Peek should provide a normal character (%c)", c1);
|
|
TEST(stream.position == old_position,
|
|
"Peek should not shift the position (%lu -> %lu)", old_position,
|
|
stream.position);
|
|
|
|
char c2 = stream_next(&stream);
|
|
TEST(c2 != '\0', "Next should provide a normal character (%c)", c2);
|
|
TEST(stream.position > old_position,
|
|
"Next should shift the position (%lu -> %lu)", old_position,
|
|
stream.position);
|
|
TEST(c2 != c1,
|
|
"Next should yield a different character (%c) to the previous peek "
|
|
"(%c)",
|
|
c2, c1);
|
|
|
|
char c3 = stream_peek(&stream);
|
|
TEST(c3 == c2,
|
|
"Peeking should yield the same character (%c) as the previous next "
|
|
"(%c)",
|
|
c3, c2);
|
|
|
|
stream_stop(&stream);
|
|
}
|
|
|
|
// Invalid streams
|
|
{
|
|
stream_t stream = {0};
|
|
stream_init_file(&stream, NULL, invalid_fp);
|
|
char c = stream_peek(&stream);
|
|
TEST(c == '\0', "Invalid streams should have an invalid peek (%c)", c);
|
|
|
|
u64 old_position = stream.position;
|
|
c = stream_next(&stream);
|
|
TEST(c == '\0', "Invalid streams should have an invalid next (%c)", c);
|
|
TEST(old_position == stream.position,
|
|
"Next on an invalid stream should not affect position (%lu -> %lu)",
|
|
old_position, stream.position);
|
|
|
|
stream_stop(&stream);
|
|
}
|
|
TEST_END();
|
|
}
|
|
|
|
void stream_test_seek(void)
|
|
{
|
|
TEST_START();
|
|
// Seeking on invalid streams
|
|
{
|
|
stream_t stream = {0};
|
|
stream_init_file(&stream, NULL, invalid_fp);
|
|
|
|
u64 old_position = stream.position;
|
|
TEST(!stream_seek_forward(&stream, 1),
|
|
"Shouldn't be possible to seek forward on an invalid stream.");
|
|
TEST(old_position == stream.position,
|
|
"Position shouldn't be affected when seeking forward on an invalid "
|
|
"stream"
|
|
"(%lu -> %lu)",
|
|
old_position, stream.position);
|
|
|
|
TEST(!stream_seek_backward(&stream, 1),
|
|
"Shouldn't be possible to seek backward on an invalid stream.");
|
|
TEST(old_position == stream.position,
|
|
"Position shouldn't be affected when seeking backward on an invalid "
|
|
"stream (%lu -> %lu)",
|
|
old_position, stream.position);
|
|
|
|
stream_stop(&stream);
|
|
}
|
|
|
|
// Valid streams
|
|
{
|
|
stream_t stream = {0};
|
|
stream_init_file(&stream, valid_filename, valid_fp);
|
|
|
|
u64 old_position = stream.position;
|
|
TEST(stream_seek_forward(&stream, 1),
|
|
"Okay to seek forward on a valid stream.");
|
|
TEST(old_position < stream.position,
|
|
"Position should be greater than before when seeking forward on a "
|
|
"valid stream (%lu -> %lu)",
|
|
old_position, stream.position);
|
|
|
|
TEST(stream_seek_backward(&stream, 1),
|
|
"Okay to seek backward on a valid stream.");
|
|
TEST(old_position == stream.position,
|
|
"stream_seek_forward and stream_seek_backward are inverse operations");
|
|
|
|
u64 forward_offset = stream_seek_forward(&stream, ARRSIZE(words_text) * 2);
|
|
TEST(forward_offset < ARRSIZE(words_text) * 2,
|
|
"Forward seeking by offsets greater than file size clamps (%lu "
|
|
"clamps to %lu)",
|
|
ARRSIZE(words_text) * 2, forward_offset);
|
|
|
|
u64 file_size = stream_size(&stream);
|
|
u64 backward_offset = stream_seek_backward(&stream, file_size + 1);
|
|
TEST(backward_offset == file_size,
|
|
"Backward seeking by offsets greater than file size clamps (%lu "
|
|
"clamps to %lu)",
|
|
file_size + 1, backward_offset);
|
|
|
|
TEST(stream.position == 0,
|
|
"Clamped forward and clamped backward seeking "
|
|
"leads to start of stream (position=%lu)",
|
|
stream.position);
|
|
|
|
i64 r_forward_offset = (rand() % (file_size - 1)) + 1;
|
|
i64 r_backward_offset = (rand() % (file_size - 1)) + 1;
|
|
while (r_backward_offset >= r_forward_offset)
|
|
r_backward_offset = (rand() % (file_size - 1)) + 1;
|
|
|
|
TEST(stream_seek(&stream, r_forward_offset) == (u64)r_forward_offset,
|
|
"Seeking by a random positive offset (%lu) is valid",
|
|
r_forward_offset);
|
|
|
|
TEST(stream_seek(&stream, -r_backward_offset) == (u64)r_backward_offset,
|
|
"Seeking backward by a random negative offset (%lu) is valid",
|
|
r_backward_offset);
|
|
|
|
TEST(
|
|
(i64)stream.position == r_forward_offset - r_backward_offset,
|
|
"Stream position (%lu) is exactly shifted by seeking offsets described "
|
|
"above.",
|
|
stream.position);
|
|
|
|
stream_stop(&stream);
|
|
}
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
void stream_test_substr(void)
|
|
{
|
|
TEST_START();
|
|
u64 size = rand() % (ARRSIZE(words_text) - 1);
|
|
u64 position = ARRSIZE(words_text) - size - 1;
|
|
|
|
// Taking substrings of invalid streams
|
|
{
|
|
stream_t stream = {0};
|
|
stream_init_file(&stream, NULL, invalid_fp);
|
|
|
|
// Relative
|
|
{
|
|
sv_t result = stream_substr(&stream, size);
|
|
TEST(result.data == NULL && result.size == 0,
|
|
"Relative substring with size %lu on invalid stream should be NULL",
|
|
size);
|
|
}
|
|
|
|
// Absolute
|
|
{
|
|
sv_t result = stream_substr_abs(&stream, position, size);
|
|
TEST(result.data == NULL && result.size == 0,
|
|
"Absolute substring @%lu with size %lu on invalid stream should be "
|
|
"NULL",
|
|
position, size);
|
|
}
|
|
|
|
stream_stop(&stream);
|
|
}
|
|
|
|
// Taking substrings of valid streams
|
|
{
|
|
stream_t stream = {0};
|
|
stream_init_file(&stream, valid_filename, valid_fp);
|
|
|
|
// Absolute
|
|
{
|
|
sv_t result = stream_substr_abs(&stream, position, size);
|
|
TEST(result.data && result.size,
|
|
"Absolute substring @%lu with size %lu on valid stream should be "
|
|
"nonzero",
|
|
position, size);
|
|
|
|
TEST(result.size == size, "Substring has right size (%lu)", result.size);
|
|
|
|
sv_t expected = SV((char *)words_text + position, size);
|
|
TEST(strncmp(result.data, expected.data, result.size) == 0,
|
|
"Expect the substring to be the same as the data we put in");
|
|
}
|
|
|
|
// Relative
|
|
{
|
|
sv_t result = stream_substr(&stream, size);
|
|
TEST(result.data && result.size,
|
|
"Relative substring with size %lu should be nonzero", size);
|
|
|
|
TEST(result.size == size, "Substring has right size (%lu)", result.size);
|
|
|
|
sv_t expected = SV((char *)words_text, size);
|
|
TEST(strncmp(result.data, expected.data, result.size) == 0,
|
|
"Expect the substring to be the same as the data we put in");
|
|
}
|
|
|
|
// Relative substring after seeking
|
|
{
|
|
// Shift forward to a random position
|
|
assert(stream_seek_forward(&stream, position)); // not a test
|
|
sv_t result = stream_substr(&stream, size);
|
|
TEST(result.data && result.size,
|
|
"Relative substring with size %lu after seeking %lu bytes should be "
|
|
"nonzero",
|
|
size, position);
|
|
|
|
TEST(result.size == size, "Substring has right size (%lu)", result.size);
|
|
|
|
sv_t expected = SV((char *)words_text + position, size);
|
|
TEST(strncmp(result.data, expected.data, result.size) == 0,
|
|
"Expect the substring to be the same as the data we put in");
|
|
|
|
// Shift back to the original position.
|
|
assert(stream_seek_backward(&stream, position)); // not a test
|
|
}
|
|
|
|
// Bad substrings
|
|
{
|
|
{
|
|
sv_t result = stream_substr_abs(&stream, stream_size(&stream), 100);
|
|
TEST(!result.data && !result.size,
|
|
"Absolute substring at %lu of 100 bytes is invalid",
|
|
stream_size(&stream));
|
|
}
|
|
|
|
assert(stream_seek_forward(&stream, stream_size(&stream))); // not a test
|
|
{
|
|
sv_t result = stream_substr(&stream, 100);
|
|
TEST(!result.data && !result.size,
|
|
"Relative substring with size 100 after seeking %lu bytes is "
|
|
"invalid",
|
|
stream.position);
|
|
}
|
|
}
|
|
|
|
stream_stop(&stream);
|
|
}
|
|
TEST_END();
|
|
}
|
|
|
|
void stream_test_till(void)
|
|
{
|
|
TEST_START();
|
|
TODO("Not implemented");
|
|
}
|
|
|
|
void stream_test_while(void)
|
|
{
|
|
TEST_START();
|
|
TODO("Not implemented");
|
|
}
|
|
|
|
void stream_test_line_col(void)
|
|
{
|
|
TEST_START();
|
|
TODO("Not implemented");
|
|
}
|
|
|
|
MAKE_TEST_SUITE(STREAM_SUITE, "Stream Tests",
|
|
|
|
MAKE_TEST_FN(stream_test_prologue),
|
|
MAKE_TEST_FN(stream_test_string),
|
|
MAKE_TEST_FN(stream_test_file),
|
|
MAKE_TEST_FN(stream_test_peek_next),
|
|
MAKE_TEST_FN(stream_test_seek),
|
|
MAKE_TEST_FN(stream_test_substr),
|
|
MAKE_TEST_FN(stream_test_epilogue), );
|
|
|
|
/* Copyright (C) 2026 Aryadev Chavali
|
|
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
|
|
* details.
|
|
|
|
* You may distribute and modify this code under the terms of the GNU General
|
|
* Public License Version 2, which you should have received a copy of along with
|
|
* this program. If not, please go to <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|