TestSuite
Some checks failed
CI / build-linux-and-wasm (x64-linux) (push) Has been cancelled

This commit is contained in:
2026-01-23 05:52:26 +05:30
parent fc18c8667a
commit cfa0759133
37 changed files with 5631 additions and 1083 deletions

View File

@ -139,8 +139,7 @@ auto AsyncOps::wait_for_schedule_completion(Schedule *schedule) -> void {
}
auto AsyncOps::get_worker_count() -> WorkerId {
// +1 for MainThread (Work Stealing)
return static_cast<WorkerId>(s_schedule_workers.size() + 1);
return static_cast<WorkerId>(s_schedule_workers.size());
}
auto AsyncOps::schedule_worker_loop(std::stop_token stop_token,

View File

@ -15,16 +15,12 @@
#include <IACore/CLI.hpp>
namespace IACore
{
CLIParser::CLIParser(Span<const String> args) : m_argList(args)
{
assert(args.size());
namespace IACore {
CLIParser::CLIParser(Span<const String> args) : m_arg_list(args) {
m_current_arg = m_arg_list.begin();
m_currentArg = m_argList.begin();
// Skip executable path
if (m_currentArg != m_argList.end())
m_currentArg++;
}
// Skip executable path
if (m_current_arg != m_arg_list.end())
m_current_arg++;
}
} // namespace IACore

View File

@ -68,7 +68,7 @@ auto FileOps::map_shared_memory(const String &name, usize size, bool is_owner)
(DWORD)(size >> 32), (DWORD)(size & 0xFFFFFFFF),
w_name.c_str());
} else {
h_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, w_name.c_str());
h_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, false, w_name.c_str());
}
if (h_map == NULL) {
@ -201,7 +201,7 @@ auto FileOps::stream_to_file(const Path &path, bool overwrite)
if (!overwrite && std::filesystem::exists(path)) {
return fail("File already exists: {}", path.string());
}
return StreamWriter::create(path);
return StreamWriter::create_from_file(path);
}
auto FileOps::stream_from_file(const Path &path) -> Result<StreamReader> {

View File

@ -133,7 +133,7 @@ void ProcessOps::terminate_process(const Box<ProcessHandle> &handle) {
}
#if IA_PLATFORM_WINDOWS
HANDLE h_process = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
HANDLE h_process = OpenProcess(PROCESS_TERMINATE, false, pid);
if (h_process != NULL) {
::TerminateProcess(h_process, 9);
CloseHandle(h_process);
@ -150,7 +150,7 @@ auto ProcessOps::spawn_process_windows(
std::atomic<NativeProcessID> &id) -> Result<i32> {
#if IA_PLATFORM_WINDOWS
SECURITY_ATTRIBUTES sa_attr = {sizeof(SECURITY_ATTRIBUTES), NULL,
TRUE}; // Allow inheritance
true}; // Allow inheritance
HANDLE h_read = NULL;
HANDLE h_write = NULL;
@ -174,7 +174,7 @@ auto ProcessOps::spawn_process_windows(
// Windows command line needs to be mutable and concatenated
String command_line = std::format("\"{}\" {}", command, args);
BOOL success = CreateProcessA(NULL, command_line.data(), NULL, NULL, TRUE, 0,
BOOL success = CreateProcessA(NULL, command_line.data(), NULL, NULL, true, 0,
NULL, NULL, &si, &pi);
// Close write end in parent, otherwise ReadFile never returns EOF!

View File

@ -40,6 +40,41 @@ StreamReader::StreamReader(Span<const u8> data)
: m_data(data.data()), m_data_size(data.size()),
m_storage_type(StorageType::NonOwning) {}
StreamReader::StreamReader(StreamReader &&other)
: m_data(other.m_data), m_cursor(other.m_cursor),
m_data_size(other.m_data_size),
m_owning_vector(std::move(other.m_owning_vector)),
m_storage_type(other.m_storage_type) {
other.m_storage_type = StorageType::NonOwning;
other.m_data = {};
other.m_data_size = 0;
if (m_storage_type == StorageType::OwningVector)
m_data = m_owning_vector.data();
}
auto StreamReader::operator=(StreamReader &&other) -> StreamReader & {
if (this != &other) {
if (m_storage_type == StorageType::OwningMmap) {
FileOps::unmap_file(m_data);
}
m_data = other.m_data;
m_cursor = other.m_cursor;
m_data_size = other.m_data_size;
m_owning_vector = std::move(other.m_owning_vector);
m_storage_type = other.m_storage_type;
if (m_storage_type == StorageType::OwningVector)
m_data = m_owning_vector.data();
other.m_storage_type = StorageType::NonOwning;
other.m_data = {};
other.m_data_size = 0;
}
return *this;
}
StreamReader::~StreamReader() {
if (m_storage_type == StorageType::OwningMmap) {
FileOps::unmap_file(m_data);

View File

@ -17,9 +17,7 @@
namespace IACore {
auto StreamWriter::create(const Path &path) -> Result<StreamWriter> {
// Try to open the file to ensure we have write permissions and to truncate
// it.
auto StreamWriter::create_from_file(const Path &path) -> Result<StreamWriter> {
FILE *f = std::fopen(path.string().c_str(), "wb");
if (!f) {
return fail("Failed to open file for writing: {}", path.string());
@ -43,34 +41,97 @@ StreamWriter::StreamWriter(Span<u8> data)
: m_buffer(data.data()), m_cursor(0), m_capacity(data.size()),
m_storage_type(StorageType::NonOwning) {}
StreamWriter::~StreamWriter() {
if (m_storage_type == StorageType::OwningFile) {
if (m_file_path.empty()) {
return;
StreamWriter::StreamWriter(StreamWriter &&other)
: m_buffer(other.m_buffer), m_cursor(other.m_cursor),
m_capacity(other.m_capacity), m_file_path(other.m_file_path),
m_owning_vector(std::move(other.m_owning_vector)),
m_storage_type(other.m_storage_type) {
other.m_capacity = {};
other.m_buffer = {};
other.m_storage_type = StorageType::NonOwning;
if (m_storage_type == StorageType::OwningVector)
m_buffer = m_owning_vector.data();
}
auto StreamWriter::operator=(StreamWriter &&other) -> StreamWriter & {
if (this != &other) {
// Flush current if needed
if (m_storage_type == StorageType::OwningFile) {
if (auto res = flush_to_disk(); !res) {
std::fprintf(stderr, "[IACore] Data loss in StreamWriter move: %s\n",
res.error().c_str());
}
}
FILE *f = std::fopen(m_file_path.string().c_str(), "wb");
if (f) {
// Write the actual data accumulated in the buffer.
std::fwrite(m_buffer, 1, m_cursor, f);
std::fclose(f);
} else {
// Logger is disabled, print to stderr as a fallback for data loss
// warning.
std::fprintf(stderr, "[IACore] StreamWriter failed to save file: %s\n",
m_file_path.string().c_str());
m_buffer = other.m_buffer;
m_cursor = other.m_cursor;
m_capacity = other.m_capacity;
m_file_path = std::move(other.m_file_path); // Use move for string/path
m_owning_vector = std::move(other.m_owning_vector);
m_storage_type = other.m_storage_type;
if (m_storage_type == StorageType::OwningVector)
m_buffer = m_owning_vector.data();
other.m_capacity = 0;
other.m_cursor = 0;
other.m_buffer = nullptr;
other.m_storage_type = StorageType::NonOwning;
}
return *this;
}
StreamWriter::~StreamWriter() {
if (m_storage_type == StorageType::OwningFile) {
// We can't return errors here, so we log them.
// Ideally, the user calls save() before this runs.
if (auto res = flush_to_disk(); !res) {
std::fprintf(stderr, "[IACore] LOST DATA in ~StreamWriter: %s\n",
res.error().c_str());
}
}
}
auto StreamWriter::flush() -> Result<void> {
auto res = flush_to_disk();
// Prevent double-write in destructor if save was successful
if (res.has_value()) {
m_storage_type = StorageType::OwningVector; // downgrade to vector so dtor
// doesn't write again
}
return res;
}
auto StreamWriter::flush_to_disk() -> Result<void> {
if (m_storage_type != StorageType::OwningFile || m_file_path.empty()) {
return {};
}
FILE *f = std::fopen(m_file_path.string().c_str(), "wb");
if (!f) {
return fail("Failed to open file for writing: {}", m_file_path.string());
}
usize written = std::fwrite(m_buffer, 1, m_cursor, f);
std::fclose(f);
if (written != m_cursor) {
return fail("Incomplete write: {} of {} bytes written", written, m_cursor);
}
return {};
}
auto StreamWriter::write(u8 byte, usize count) -> Result<void> {
if (m_cursor + count > m_capacity) {
if (m_storage_type == StorageType::NonOwning) {
return fail("StreamWriter buffer overflow (NonOwning)");
}
// Growth strategy: Current capacity + (count * 2)
const usize new_capacity = m_capacity + (count << 1);
const usize required = m_cursor + count;
const usize double_cap = m_capacity * 2;
const usize new_capacity = (double_cap > required) ? double_cap : required;
m_owning_vector.resize(new_capacity);
m_capacity = m_owning_vector.size();
m_buffer = m_owning_vector.data();
@ -87,7 +148,12 @@ auto StreamWriter::write(const void *buffer, usize size) -> Result<void> {
return fail("StreamWriter buffer overflow (NonOwning)");
}
const usize new_capacity = m_capacity + (size << 1);
// NEW STRATEGY: Max(Double Capacity, Required Size)
// This prevents frequent reallocations for repeated small writes
const usize required = m_cursor + size;
const usize double_cap = m_capacity * 2;
const usize new_capacity = (double_cap > required) ? double_cap : required;
m_owning_vector.resize(new_capacity);
m_capacity = m_owning_vector.size();
m_buffer = m_owning_vector.data();

View File

@ -16,28 +16,53 @@
#include <IACore/StringOps.hpp>
namespace IACore {
const String BASE64_CHAR_TABLE =
static const String BASE64_CHAR_TABLE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static auto is_base64(u8 c) -> bool {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || (c == '+') || (c == '/');
}
static auto get_base64_index(u8 c) -> u8 {
if (c >= 'A' && c <= 'Z')
return c - 'A';
if (c >= 'a' && c <= 'z')
return c - 'a' + 26;
if (c >= '0' && c <= '9')
return c - '0' + 52;
if (c == '+')
return 62;
if (c == '/')
return 63;
return 0;
}
auto StringOps::encode_base64(Span<const u8> data) -> String {
String result;
result.reserve(((data.size() + 2) / 3) * 4);
for (size_t i = 0; i < data.size(); i += 3) {
uint32_t value = 0;
i32 num_bytes = 0;
for (i32 j = 0; j < 3 && (i + j) < data.size(); ++j) {
value = (value << 8) | data[i + j];
num_bytes++;
for (usize i = 0; i < data.size(); i += 3) {
u32 b0 = data[i];
u32 b1 = (i + 1 < data.size()) ? data[i + 1] : 0;
u32 b2 = (i + 2 < data.size()) ? data[i + 2] : 0;
u32 triple = (b0 << 16) | (b1 << 8) | b2;
result += BASE64_CHAR_TABLE[(triple >> 18) & 0x3F];
result += BASE64_CHAR_TABLE[(triple >> 12) & 0x3F];
if (i + 1 < data.size()) {
result += BASE64_CHAR_TABLE[(triple >> 6) & 0x3F];
} else {
result += '=';
}
for (i32 j = 0; j < num_bytes + 1; ++j) {
if (j < 4) {
result += BASE64_CHAR_TABLE[(value >> (6 * (3 - j))) & 0x3F];
}
}
if (num_bytes < 3) {
for (i32 j = 0; j < (3 - num_bytes); ++j) {
result += '=';
}
if (i + 2 < data.size()) {
result += BASE64_CHAR_TABLE[triple & 0x3F];
} else {
result += '=';
}
}
return result;
@ -45,47 +70,53 @@ auto StringOps::encode_base64(Span<const u8> data) -> String {
auto StringOps::decode_base64(const String &data) -> Vec<u8> {
Vec<u8> result;
result.reserve(data.size() * 3 / 4);
const auto is_base64 = [](u8 c) {
return (isalnum(c) || (c == '+') || (c == '/'));
};
i32 i = 0;
u8 tmp_buf[4];
i32 in_len = data.size();
i32 i = 0, j = 0, in = 0;
u8 tmp_buf0[4], tmp_buf1[3];
for (char c_char : data) {
u8 c = static_cast<u8>(c_char);
if (c == '=') {
break;
}
if (!is_base64(c)) {
break;
}
while (in_len-- && (data[in] != '=') && is_base64(data[in])) {
tmp_buf0[i++] = data[in];
in++;
tmp_buf[i++] = c;
if (i == 4) {
for (i = 0; i < 4; i++)
tmp_buf0[i] = BASE64_CHAR_TABLE.find(tmp_buf0[i]);
u8 n0 = get_base64_index(tmp_buf[0]);
u8 n1 = get_base64_index(tmp_buf[1]);
u8 n2 = get_base64_index(tmp_buf[2]);
u8 n3 = get_base64_index(tmp_buf[3]);
tmp_buf1[0] = (tmp_buf0[0] << 2) + ((tmp_buf0[1] & 0x30) >> 4);
tmp_buf1[1] = ((tmp_buf0[1] & 0xf) << 4) + ((tmp_buf0[2] & 0x3c) >> 2);
tmp_buf1[2] = ((tmp_buf0[2] & 0x3) << 6) + tmp_buf0[3];
result.push_back((n0 << 2) | ((n1 & 0x30) >> 4));
result.push_back(((n1 & 0x0F) << 4) | ((n2 & 0x3C) >> 2));
result.push_back(((n2 & 0x03) << 6) | n3);
for (i = 0; (i < 3); i++)
result.push_back(tmp_buf1[i]);
i = 0;
}
}
if (i) {
for (j = i; j < 4; j++)
tmp_buf0[j] = 0;
if (i > 0) {
for (i32 j = i; j < 4; ++j) {
tmp_buf[j] = 'A'; // Pad with 'A' (index 0)
}
for (j = 0; j < 4; j++)
tmp_buf0[j] = BASE64_CHAR_TABLE.find(tmp_buf0[j]);
u8 n0 = get_base64_index(tmp_buf[0]);
u8 n1 = get_base64_index(tmp_buf[1]);
u8 n2 = get_base64_index(tmp_buf[2]);
tmp_buf1[0] = (tmp_buf0[0] << 2) + ((tmp_buf0[1] & 0x30) >> 4);
tmp_buf1[1] = ((tmp_buf0[1] & 0xf) << 4) + ((tmp_buf0[2] & 0x3c) >> 2);
tmp_buf1[2] = ((tmp_buf0[2] & 0x3) << 6) + tmp_buf0[3];
for (j = 0; (j < i - 1); j++)
result.push_back(tmp_buf1[j]);
if (i > 1) {
result.push_back((n0 << 2) | ((n1 & 0x30) >> 4));
}
if (i > 2) {
result.push_back(((n1 & 0x0F) << 4) | ((n2 & 0x3C) >> 2));
}
}
return result;
}
} // namespace IACore

View File

@ -93,7 +93,7 @@ private:
// Implementation
// =============================================================================
auto RingBufferView::default_instance() -> RingBufferView {
inline auto RingBufferView::default_instance() -> RingBufferView {
return RingBufferView(nullptr, {}, false);
}
@ -262,7 +262,7 @@ inline auto RingBufferView::read_wrapped(u32 offset, void *out_data, u32 size)
}
}
[[nodiscard]] auto RingBufferView::is_valid() const -> bool {
[[nodiscard]] inline auto RingBufferView::is_valid() const -> bool {
return m_control_block && m_data_ptr && m_capacity;
}

View File

@ -17,54 +17,47 @@
#include <IACore/PCH.hpp>
namespace IACore
{
class CLIParser
{
/*
* PLEASE READ
*
* CLIParser is still very much in it's baby stages.
* Subject to heavy and frequent changes, use with
* caution!
*/
namespace IACore {
class CLIParser {
/*
* PLEASE READ
*
* CLIParser is still very much in it's baby stages.
* Subject to heavy and frequent changes, use with
* caution!
*/
public:
CLIParser(Span<const String> args);
~CLIParser() = default;
public:
CLIParser(Span<const String> args);
~CLIParser() = default;
public:
auto remaining() const -> bool
{
return m_currentArg < m_argList.end();
}
public:
[[nodiscard]] auto remaining() const -> bool {
return m_current_arg < m_arg_list.end();
}
auto peek() const -> StringView
{
if (!remaining())
return "";
return *m_currentArg;
}
[[nodiscard]] auto peek() const -> StringView {
if (!remaining())
return "";
return *m_current_arg;
}
auto next() -> StringView
{
if (!remaining())
return "";
return *m_currentArg++;
}
auto next() -> StringView {
if (!remaining())
return "";
return *m_current_arg++;
}
auto consume(const StringView &expected) -> bool
{
if (peek() == expected)
{
next();
return true;
}
return false;
}
auto consume(const StringView &expected) -> bool {
if (peek() == expected) {
next();
return true;
}
return false;
}
private:
const Span<const String> m_argList;
Span<const String>::const_iterator m_currentArg;
};
private:
const Span<const String> m_arg_list;
Span<const String>::const_iterator m_current_arg;
};
} // namespace IACore

View File

@ -21,295 +21,271 @@
// Macros
// -----------------------------------------------------------------------------
#define __iat_micro_test(call) \
if (!(call)) \
return false
#define __iat_micro_test(call) \
if (!(call)) \
return false
#define IAT_CHECK(v) __iat_micro_test(_test((v), #v))
#define IAT_CHECK_NOT(v) __iat_micro_test(_test_not((v), "NOT " #v))
#define IAT_CHECK_EQ(lhs, rhs) __iat_micro_test(_test_eq((lhs), (rhs), #lhs " == " #rhs))
#define IAT_CHECK_NEQ(lhs, rhs) __iat_micro_test(_test_neq((lhs), (rhs), #lhs " != " #rhs))
#define IAT_CHECK_EQ(lhs, rhs) \
__iat_micro_test(_test_eq((lhs), (rhs), #lhs " == " #rhs))
#define IAT_CHECK_NEQ(lhs, rhs) \
__iat_micro_test(_test_neq((lhs), (rhs), #lhs " != " #rhs))
#define IAT_CHECK_APPROX(lhs, rhs) __iat_micro_test(_test_approx((lhs), (rhs), #lhs " ~= " #rhs))
#define IAT_CHECK_APPROX(lhs, rhs) \
__iat_micro_test(_test_approx((lhs), (rhs), #lhs " ~= " #rhs))
#define IAT_UNIT(func) _test_unit([this]() { return this->func(); }, #func)
#define IAT_NAMED_UNIT(n, func) _test_unit([this]() { return this->func(); }, n)
#define IAT_BLOCK(name) class name : public ia::iatest::Block
#define IAT_BLOCK(name) class name : public IACore::Test::Block
#define IAT_BEGIN_BLOCK(_group, _name) \
class _group##_##_name : public ia::iatest::Block \
{ \
public: \
[[nodiscard]] auto get_name() const -> const char * override \
{ \
return #_group "::" #_name; \
} \
\
private:
#define IAT_END_BLOCK() \
} \
;
#define IAT_BEGIN_TEST_LIST() \
public: \
void declare_tests() override \
{
#define IAT_ADD_TEST(name) IAT_UNIT(name)
#define IAT_END_TEST_LIST() \
} \
\
#define IAT_BEGIN_BLOCK(_group, _name) \
class _group##_##_name : public IACore::Test::Block { \
public: \
[[nodiscard]] auto get_name() const -> const char * override { \
return #_group "::" #_name; \
} \
\
private:
namespace IACore
{
// -------------------------------------------------------------------------
// String Conversion Helpers
// -------------------------------------------------------------------------
template<typename T> auto to_string(const T &value) -> String
{
if constexpr (std::is_arithmetic_v<T>)
{
return std::to_string(value);
}
else if constexpr (std::is_same_v<T, String> || std::is_same_v<T, const char *> || std::is_same_v<T, char *>)
{
return String("\"") + String(value) + "\"";
}
else
{
return "{Object}";
}
#define IAT_END_BLOCK() \
} \
;
#define IAT_BEGIN_TEST_LIST() \
public: \
void declare_tests() override {
#define IAT_ADD_TEST(name) IAT_UNIT(name)
#define IAT_END_TEST_LIST() \
} \
\
private:
namespace IACore::Test {
// -------------------------------------------------------------------------
// String Conversion Helpers
// -------------------------------------------------------------------------
template <typename T> auto to_string(const T &value) -> String {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, String> ||
std::is_same_v<T, const char *> ||
std::is_same_v<T, char *>) {
return String("\"") + String(value) + "\"";
} else {
return "{Object}";
}
}
template <typename T> auto to_string(T *value) -> String {
if (value == nullptr) {
return "nullptr";
}
return std::format("ptr({})", static_cast<const void *>(value));
}
// -------------------------------------------------------------------------
// Types
// -------------------------------------------------------------------------
using TestFunctor = std::function<bool()>;
struct TestUnit {
String name;
TestFunctor functor;
};
class Block {
public:
virtual ~Block() = default;
[[nodiscard]] virtual auto get_name() const -> const char * = 0;
virtual void declare_tests() = 0;
auto units() -> Vec<TestUnit> & { return m_units; }
protected:
template <typename T1, typename T2>
auto _test_eq(const T1 &lhs, const T2 &rhs, const char *description) -> bool {
if (lhs != rhs) {
print_fail(description, to_string(lhs), to_string(rhs));
return false;
}
return true;
}
template <typename T1, typename T2>
auto _test_neq(const T1 &lhs, const T2 &rhs, const char *description)
-> bool {
if (lhs == rhs) {
print_fail(description, to_string(lhs), "NOT " + to_string(rhs));
return false;
}
return true;
}
template <typename T>
auto _test_approx(T lhs, T rhs, const char *description) -> bool {
static_assert(std::is_floating_point_v<T>,
"Approx only works for floats/doubles");
T diff = std::abs(lhs - rhs);
if (diff > static_cast<T>(0.0001)) {
print_fail(description, to_string(lhs), to_string(rhs));
return false;
}
return true;
}
auto _test(bool value, const char *description) -> bool {
if (!value) {
std::cout << console::BLUE << " " << description << "... "
<< console::RED << "FAILED" << console::RESET << "\n";
return false;
}
return true;
}
auto _test_not(bool value, const char *description) -> bool {
if (value) {
std::cout << console::BLUE << " " << description << "... "
<< console::RED << "FAILED" << console::RESET << "\n";
return false;
}
return true;
}
void _test_unit(TestFunctor functor, const char *name) {
m_units.push_back({name, std::move(functor)});
}
private:
void print_fail(const char *desc, const String &v1, const String &v2) {
std::cout << console::BLUE << " " << desc << "... " << console::RED
<< "FAILED" << console::RESET << "\n";
std::cout << console::RED << " Expected: " << v2 << console::RESET
<< "\n";
std::cout << console::RED << " Actual: " << v1 << console::RESET
<< "\n";
}
Vec<TestUnit> m_units;
};
template <typename T>
concept ValidBlockClass = std::derived_from<T, Block>;
// -------------------------------------------------------------------------
// Runner
// -------------------------------------------------------------------------
template <bool StopOnFail = false, bool IsVerbose = false> class Runner {
public:
Runner() = default;
~Runner() { summarize(); }
template <typename BlockClass>
requires ValidBlockClass<BlockClass>
void test_block();
private:
void summarize();
usize m_test_count{0};
usize m_fail_count{0};
usize m_block_count{0};
};
template <bool StopOnFail, bool IsVerbose>
template <typename BlockClass>
requires ValidBlockClass<BlockClass>
void Runner<StopOnFail, IsVerbose>::test_block() {
m_block_count++;
BlockClass b;
b.declare_tests();
std::cout << console::MAGENTA << "Testing [" << b.get_name() << "]..."
<< console::RESET << "\n";
for (auto &v : b.units()) {
m_test_count++;
if constexpr (IsVerbose) {
std::cout << console::YELLOW << " Testing " << v.name << "...\n"
<< console::RESET;
}
template<typename T> auto to_string(T *value) -> String
{
if (value == nullptr)
{
return "nullptr";
}
return std::format("ptr({})", static_cast<const void *>(value));
// Exceptions are DISABLED. We assume tests do not crash.
// If a test crashes (segfault), the OS handles it.
bool result = v.functor();
if (!result) {
m_fail_count++;
if constexpr (StopOnFail) {
summarize();
std::exit(-1);
}
}
}
std::cout << "\n";
}
template <bool StopOnFail, bool IsVerbose>
void Runner<StopOnFail, IsVerbose>::summarize() {
std::cout << console::GREEN
<< "\n-----------------------------------\n\t "
"SUMMARY\n-----------------------------------\n";
if (m_fail_count == 0) {
std::cout << "\n\tALL TESTS PASSED!\n\n";
} else {
f64 success_rate = (100.0 * static_cast<f64>(m_test_count - m_fail_count) /
static_cast<f64>(m_test_count));
std::cout << console::RED << m_fail_count << " OF " << m_test_count
<< " TESTS FAILED\n"
<< console::YELLOW
<< std::format("Success Rate: {:.2f}%\n", success_rate);
}
std::cout << console::MAGENTA << "Ran " << m_test_count << " test(s) across "
<< m_block_count << " block(s)\n"
<< console::GREEN << "-----------------------------------"
<< console::RESET << "\n";
}
using DefaultRunner = Runner<false, true>;
// -------------------------------------------------------------------------
// Registry
// -------------------------------------------------------------------------
class TestRegistry {
public:
using TestEntry = std::function<void(DefaultRunner &)>;
static auto get_entries() -> Vec<TestEntry> & {
static Vec<TestEntry> entries;
return entries;
}
static auto run_all() -> i32 {
DefaultRunner r;
auto &entries = get_entries();
std::cout << console::CYAN << "[IATest] Discovered " << entries.size()
<< " Test Blocks\n\n"
<< console::RESET;
for (auto &entry : entries) {
entry(r);
}
// -------------------------------------------------------------------------
// Types
// -------------------------------------------------------------------------
using TestFunctor = std::function<bool()>;
return 0;
}
};
struct TestUnit
{
String name;
TestFunctor functor;
};
template <typename BlockType> struct AutoRegister {
AutoRegister() {
TestRegistry::get_entries().push_back(
[](DefaultRunner &r) { r.test_block<BlockType>(); });
}
};
} // namespace IACore::Test
class Block
{
public:
virtual ~Block() = default;
[[nodiscard]] virtual auto get_name() const -> const char * = 0;
virtual void declare_tests() = 0;
auto units() -> Vec<TestUnit> &
{
return m_units;
}
protected:
template<typename T1, typename T2> auto _test_eq(const T1 &lhs, const T2 &rhs, const char *description) -> bool
{
if (lhs != rhs)
{
print_fail(description, to_string(lhs), to_string(rhs));
return false;
}
return true;
}
template<typename T1, typename T2> auto _test_neq(const T1 &lhs, const T2 &rhs, const char *description) -> bool
{
if (lhs == rhs)
{
print_fail(description, to_string(lhs), "NOT " + to_string(rhs));
return false;
}
return true;
}
template<typename T> auto _test_approx(T lhs, T rhs, const char *description) -> bool
{
static_assert(std::is_floating_point_v<T>, "Approx only works for floats/doubles");
T diff = std::abs(lhs - rhs);
if (diff > static_cast<T>(0.0001))
{
print_fail(description, to_string(lhs), to_string(rhs));
return false;
}
return true;
}
auto _test(bool value, const char *description) -> bool
{
if (!value)
{
std::cout << console::blue << " " << description << "... " << console::red << "FAILED"
<< console::reset << "\n";
return false;
}
return true;
}
auto _test_not(bool value, const char *description) -> bool
{
if (value)
{
std::cout << console::blue << " " << description << "... " << console::red << "FAILED"
<< console::reset << "\n";
return false;
}
return true;
}
void _test_unit(TestFunctor functor, const char *name)
{
m_units.push_back({name, std::move(functor)});
}
private:
void print_fail(const char *desc, const String &v1, const String &v2)
{
std::cout << console::blue << " " << desc << "... " << console::red << "FAILED" << console::reset
<< "\n";
std::cout << console::red << " Expected: " << v2 << console::reset << "\n";
std::cout << console::red << " Actual: " << v1 << console::reset << "\n";
}
Vec<TestUnit> m_units;
};
template<typename T>
concept ValidBlockClass = std::derived_from<T, Block>;
// -------------------------------------------------------------------------
// Runner
// -------------------------------------------------------------------------
template<bool StopOnFail = false, bool IsVerbose = false> class Runner
{
public:
Runner() = default;
~Runner()
{
summarize();
}
template<typename BlockClass>
requires ValidBlockClass<BlockClass>
void test_block();
private:
void summarize();
usize m_test_count{0};
usize m_fail_count{0};
usize m_block_count{0};
};
template<bool StopOnFail, bool IsVerbose>
template<typename BlockClass>
requires ValidBlockClass<BlockClass>
void Runner<StopOnFail, IsVerbose>::test_block()
{
m_block_count++;
BlockClass b;
b.declare_tests();
std::cout << console::magenta << "Testing [" << b.get_name() << "]..." << console::reset << "\n";
for (auto &v : b.units())
{
m_test_count++;
if constexpr (IsVerbose)
{
std::cout << console::yellow << " Testing " << v.name << "...\n" << console::reset;
}
// Exceptions are DISABLED. We assume tests do not crash.
// If a test crashes (segfault), the OS handles it.
bool result = v.functor();
if (!result)
{
m_fail_count++;
if constexpr (StopOnFail)
{
summarize();
std::exit(-1);
}
}
}
std::cout << "\n";
}
template<bool StopOnFail, bool IsVerbose> void Runner<StopOnFail, IsVerbose>::summarize()
{
std::cout << console::green
<< "\n-----------------------------------\n\t SUMMARY\n-----------------------------------\n";
if (m_fail_count == 0)
{
std::cout << "\n\tALL TESTS PASSED!\n\n";
}
else
{
f64 success_rate = (100.0 * static_cast<f64>(m_test_count - m_fail_count) / static_cast<f64>(m_test_count));
std::cout << console::red << m_fail_count << " OF " << m_test_count << " TESTS FAILED\n"
<< console::yellow << std::format("Success Rate: {:.2f}%\n", success_rate);
}
std::cout << console::magenta << "Ran " << m_test_count << " test(s) across " << m_block_count << " block(s)\n"
<< console::green << "-----------------------------------" << console::reset << "\n";
}
using DefaultRunner = Runner<false, true>;
// -------------------------------------------------------------------------
// Registry
// -------------------------------------------------------------------------
class TestRegistry
{
public:
using TestEntry = std::function<void(DefaultRunner &)>;
static auto get_entries() -> Vec<TestEntry> &
{
static Vec<TestEntry> entries;
return entries;
}
static auto run_all() -> i32
{
DefaultRunner r;
auto &entries = get_entries();
std::cout << console::cyan << "[IATest] Discovered " << entries.size() << " Test Blocks\n\n"
<< console::reset;
for (auto &entry : entries)
{
entry(r);
}
return 0;
}
};
template<typename BlockType> struct AutoRegister
{
AutoRegister()
{
TestRegistry::get_entries().push_back([](DefaultRunner &r) { r.test_block<BlockType>(); });
}
};
} // namespace IACore
#define IAT_REGISTER_ENTRY(Group, Name) static ia::iatest::AutoRegister<Group##_##Name> _iat_reg_##Group##_##Name;
#define IAT_REGISTER_ENTRY(Group, Name) \
static IACore::Test::AutoRegister<Group##_##Name> _iat_reg_##Group##_##Name;

View File

@ -119,8 +119,8 @@ inline auto Json::encode(const nlohmann::json &data) -> String {
template <typename T>
inline auto Json::parse_to_struct(const String &json_str) -> Result<T> {
T result{};
// glz::read_json returns an error code (bool-like optional)
const auto err = glz::read_json<GLAZE_OPTS>(result, json_str);
const auto err = glz::read<GLAZE_OPTS>(result, json_str);
if (err) {
return fail("JSON Struct Parse Error: {}",

View File

@ -34,8 +34,8 @@ public:
explicit StreamReader(Span<const u8> data);
~StreamReader();
StreamReader(StreamReader &&) = default;
auto operator=(StreamReader &&) -> StreamReader & = default;
StreamReader(StreamReader &&other);
auto operator=(StreamReader &&other) -> StreamReader &;
StreamReader(const StreamReader &) = delete;
auto operator=(const StreamReader &) -> StreamReader & = delete;
@ -86,6 +86,9 @@ inline auto StreamReader::read(void *buffer, usize size) -> Result<void> {
template <typename T>
[[nodiscard("Check for EOF")]]
inline auto StreamReader::read() -> Result<T> {
static_assert(std::is_trivially_copyable_v<T>,
"T must be trivially copyable to read via memcpy");
constexpr usize SIZE = sizeof(T);
if (m_cursor + SIZE > m_data_size) [[unlikely]] {

View File

@ -27,18 +27,19 @@ public:
OwningVector,
};
static auto create(const Path &path) -> Result<StreamWriter>;
static auto create_from_file(const Path &path) -> Result<StreamWriter>;
StreamWriter();
explicit StreamWriter(Span<u8> data);
~StreamWriter();
StreamWriter(StreamWriter &&) = default;
auto operator=(StreamWriter &&) -> StreamWriter & = default;
StreamWriter(StreamWriter &&other);
auto operator=(StreamWriter &&other) -> StreamWriter &;
StreamWriter(const StreamWriter &) = delete;
auto operator=(const StreamWriter &) -> StreamWriter & = delete;
~StreamWriter();
auto write(u8 byte, usize count) -> Result<void>;
auto write(const void *buffer, usize size) -> Result<void>;
@ -48,6 +49,8 @@ public:
[[nodiscard]] auto cursor() const -> usize { return m_cursor; }
auto flush() -> Result<void>;
private:
u8 *m_buffer = nullptr;
usize m_cursor = 0;
@ -55,6 +58,9 @@ private:
Path m_file_path;
Vec<u8> m_owning_vector;
StorageType m_storage_type = StorageType::OwningVector;
private:
auto flush_to_disk() -> Result<void>;
};
template <typename T>

File diff suppressed because it is too large Load Diff