diff --git a/.clang-format b/.clang-format index ce65514..243532f 100644 --- a/.clang-format +++ b/.clang-format @@ -162,7 +162,7 @@ MaxEmptyLinesToKeep: 1 MainIncludeChar: Any LineEnding: CRLF Language: Cpp -LambdaBodyIndentation: Signature +LambdaBodyIndentation: OuterScope KeepEmptyLines: AtEndOfFile: true AtStartOfBlock: false diff --git a/source/launch/editor/private/Main.cpp b/source/launch/editor/private/Main.cpp index 20b05e7..9900505 100644 --- a/source/launch/editor/private/Main.cpp +++ b/source/launch/editor/private/Main.cpp @@ -3,12 +3,14 @@ // mailto:support AT graphical-playground DOT com #include "container/BasicString.hpp" -#include "container/BasicStringView.hpp" #include "container/Forward.hpp" -#include "CoreMinimal.hpp" +#include "errors/Error.hpp" +#include "errors/ErrorConfig.hpp" +#include "errors/ErrorRegistry.hpp" +#include "errors/ErrorSystem.hpp" #include -int main() +int main(int argc, char* argv[]) { std::cout << "Hello, Graphical Playground Editor!" << std::endl; // TODO: Implement editor main function. @@ -16,5 +18,34 @@ int main() gp::String str = "Hello, World!"; std::cout << "gp::String says: " << str << std::endl; + auto& registry = gp::error::ErrorRegistry::instance(); + std::cout << registry.dumpAll() << std::endl; + + gp::error::ErrorSystemConfig config = gp::error::ErrorSystemConfig::getDevelopmentConfig(); + config.abort.abortFrom = gp::error::Severity::Critical; + gp::error::ErrorSystem::initialize(config); + + gp::Vector args; + for (int i = 0; i < argc; ++i) + { + args.pushBack(argv[i]); + } + + for (const auto& arg: args) + { + // clang-format off + if (arg == "--trace") GP_TRACE("This is a trace message!"); + else if (arg == "--debug") GP_DEBUG("This is a debug message!"); + else if (arg == "--info") GP_INFO("This is an info message!"); + else if (arg == "--warn") GP_WARN("This is a warning message!"); + else if (arg == "--error") GP_ERROR("This is an error message!"); + else if (arg == "--fatal") GP_FATAL("This is a fatal message!"); + else if (arg == "--panic") GP_PANIC("This is a critical message!"); + // clang-format on + } + + gp::error::ErrorSystem::flushAll(); + gp::error::ErrorSystem::shutdown(); + return 0; } diff --git a/source/runtime/core/private/errors/ErrorContext.cpp b/source/runtime/core/private/errors/ErrorContext.cpp new file mode 100644 index 0000000..35a69cf --- /dev/null +++ b/source/runtime/core/private/errors/ErrorContext.cpp @@ -0,0 +1,96 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/ErrorContext.hpp" +#include "CoreMinimal.hpp" + +namespace gp::error +{ + +void ErrorContext::push(gp::String subsystem, gp::String operation) +{ + m_stack.pushBack({ std::move(subsystem), std::move(operation), {} }); +} + +void ErrorContext::pop() noexcept +{ + if (!m_stack.isEmpty()) + { + m_stack.popBack(); + } +} + +void ErrorContext::tag(gp::String key, gp::String value) +{ + if (!m_stack.isEmpty()) + { + m_stack.back().tags.pushBack({ std::move(key), std::move(value) }); + } +} + +GP_NODISCARD bool ErrorContext::isEmpty() const noexcept +{ + return m_stack.isEmpty(); +} + +GP_NODISCARD gp::USize ErrorContext::depth() const noexcept +{ + return m_stack.size(); +} + +GP_NODISCARD const gp::Vector& ErrorContext::frames() const noexcept +{ + return m_stack; +} + +GP_NODISCARD gp::StringView ErrorContext::currentSubsystem() const noexcept +{ + return m_stack.isEmpty() ? gp::StringView{} : m_stack.back().subsystem.asView(); +} + +GP_NODISCARD MetaBag ErrorContext::flatten() const +{ + MetaBag out; + + for (gp::USize i = 0; i < m_stack.size(); ++i) + { + const auto& frame = m_stack[i]; + out.pushBack({ gp::String::format("scope[{}].subsystem", i), frame.subsystem }); + out.pushBack({ gp::String::format("scope[{}].operation", i), frame.operation }); + for (const auto& tag: frame.tags) + { + out.pushBack({ gp::String::format("scope[{}].{}", i, tag.key), tag.value }); + } + } + + return out; +} + +void ErrorContext::setThreadName(gp::String name) +{ + m_threadName = std::move(name); +} + +GP_NODISCARD const gp::String& ErrorContext::threadName() const noexcept +{ + return m_threadName; +} + +ContextScope::ContextScope(gp::String subsystem, gp::String operation) +{ + ErrorContext::current().push(std::move(subsystem), std::move(operation)); +} + +ContextScope::~ContextScope() noexcept +{ + ErrorContext::current().pop(); +} + +ContextScope& ContextScope::tag(gp::String key, gp::String value) +{ + ErrorContext::current().tag(std::move(key), std::move(value)); + return *this; +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/ErrorRecord.cpp b/source/runtime/core/private/errors/ErrorRecord.cpp new file mode 100644 index 0000000..ba30181 --- /dev/null +++ b/source/runtime/core/private/errors/ErrorRecord.cpp @@ -0,0 +1,133 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/ErrorRecord.hpp" +#include "errors/ErrorCode.hpp" +#include "errors/ErrorSeverity.hpp" +#include +#include + +namespace gp::error +{ + +void ErrorRecord::addMetadata(gp::String key, gp::String value) +{ + metadata.pushBack({ std::move(key), std::move(value) }); +} + +GP_NODISCARD gp::StringView ErrorRecord::getMetadata(gp::StringView key) const noexcept +{ + for (const auto& entry: metadata) + { + if (entry.key == key) + { + return entry.value; + } + } + return {}; +} + +GP_NODISCARD bool ErrorRecord::hasCause() const noexcept +{ + return cause != nullptr; +} + +GP_NODISCARD gp::USize ErrorRecord::causeDepth() const noexcept +{ + gp::USize depth = 0; + const ErrorRecord* current = cause.get(); + while (current) + { + ++depth; + current = current->cause.get(); + } + return depth; +} + +GP_NODISCARD gp::String ErrorRecord::summary() const +{ + return gp::String::format( + "[{}][{}:0x{:04X}] {} ({}:{})", + getSeverityName(severity), + getDomainName(code.domain()), + code.code(), + message, + location.file_name(), + location.line() + ); +} + +GP_NODISCARD gp::String ErrorRecord::fullReport() const +{ + gp::String out; + out.reserve(2048); + + out += "╔══ GP ErrorRecord ═══════════════════════════════════════════╗\n"; + out += gp::String::format("║ Severity : {}\n", getSeverityDisplay(severity)); + out += gp::String::format("║ Code : [{}] 0x{:04X}\n", getDomainName(code.domain()), code.code()); + out += gp::String::format("║ Message : {}\n", message); + out += gp::String::format( + "║ Location : {}:{} in {}\n", location.file_name(), location.line(), location.function_name() + ); + out += gp::String::format( + "║ Thread : {} ({})\n", + threadName.isEmpty() ? "unnamed" : threadName, + [&] + { + auto id = threadId; + std::hash hashThreadId; + return gp::String::format("{:#x}", hashThreadId(id)); + }() + ); + + auto tt = std::chrono::system_clock::to_time_t(wallTime); + char tbuf[64]{}; + struct tm tm{}; +#if defined(_MSC_VER) + gmtime_s(&tm, &tt); +#else + gmtime_r(&tt, &tm); +#endif + strftime(tbuf, sizeof tbuf, "%Y-%m-%d %T UTC", &tm); + out += gp::String::format("║ Timestamp : {}\n", tbuf); + + if (!metadata.isEmpty()) + { + out += "║ Meta :\n"; + for (const auto& m: metadata) + { + out += gp::String::format("║ {} = {}\n", m.key, m.value); + } + } + +#if GP_HAS_STACKTRACE + if (!stacktrace.empty()) + { + out += "║ Stacktrace:\n"; + for (const auto& frame: stacktrace) + { + out += gp::String::format("║ {}\n", std::to_string(frame)); + } + } +#endif + + if (hasCause()) + { + out += "║ Caused by :\n"; + const ErrorRecord* c = cause.get(); + int depth = 0; + while (c && depth < 8) + { + out += gp::String::format("║ [{}] {}\n", getSeverityName(c->severity), c->message); + c = c->cause.get(); + ++depth; + } + } + + out += "╚══════════════════════════════════════════════════════════════╝\n"; + + return out; +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/ErrorRegistry.cpp b/source/runtime/core/private/errors/ErrorRegistry.cpp new file mode 100644 index 0000000..708148c --- /dev/null +++ b/source/runtime/core/private/errors/ErrorRegistry.cpp @@ -0,0 +1,120 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/ErrorRegistry.hpp" +#include "container/Optional.hpp" +#include "errors/ErrorCode.hpp" + +namespace gp::error +{ + +ErrorRegistry::ErrorRegistry() +{ + registerBuiltins(); +} + +void ErrorRegistry::registerCode(ErrorCode code, ErrorEntry entry) +{ + std::lock_guard lock(m_mutex); + m_table[code.raw()] = std::move(entry); +} + +GP_NODISCARD gp::Optional ErrorRegistry::lookup(ErrorCode code) const +{ + std::lock_guard lock(m_mutex); + auto it = m_table.find(code.raw()); + if (it == m_table.end()) + { + return gp::nullOpt; + } + return it->second; +} + +GP_NODISCARD gp::String ErrorRegistry::describe(ErrorCode code) const +{ + std::lock_guard lock(m_mutex); + auto it = m_table.find(code.raw()); + if (it == m_table.end()) + { + return gp::String(""); + } + return gp::String(it->second.description); +} + +GP_NODISCARD gp::String ErrorRegistry::remediationHint(ErrorCode code) const +{ + std::lock_guard lock(m_mutex); + auto it = m_table.find(code.raw()); + if (it == m_table.end()) + { + return {}; + } + return gp::String(it->second.remediation); +} + +GP_NODISCARD gp::String ErrorRegistry::wikiUrl(ErrorCode code) const +{ + std::lock_guard lock(m_mutex); + auto it = m_table.find(code.raw()); + if (it == m_table.end()) + { + return {}; + } + return gp::String(it->second.wikiUrl); +} + +GP_NODISCARD bool ErrorRegistry::isExpected(ErrorCode code) const +{ + std::lock_guard lock(m_mutex); + auto it = m_table.find(code.raw()); + return it != m_table.end() && it->second.isExpected; +} + +GP_NODISCARD bool ErrorRegistry::isAlwaysFatal(ErrorCode code) const +{ + std::lock_guard lock(m_mutex); + auto it = m_table.find(code.raw()); + return it != m_table.end() && it->second.isAlwaysFatal; +} + +GP_NODISCARD gp::USize ErrorRegistry::size() const +{ + std::lock_guard lock(m_mutex); + return m_table.size(); +} + +GP_NODISCARD gp::String ErrorRegistry::dumpAll() const +{ + std::lock_guard lock(m_mutex); + gp::String out; + out.reserve(m_table.size() * 128); + out += gp::String::format("ErrorRegistry - {} entries\n", m_table.size()); + out += "--------------------------------\n"; + for (const auto& [raw, entry]: m_table) + { + ErrorCode code{ raw }; + out += gp::String::format( + " [{:>12}][0x{:04X}] {}\n", getDomainName(code.domain()), code.code(), entry.description + ); + if (!entry.remediation.isEmpty()) + { + out += gp::String::format(" -> {}\n", entry.remediation); + } + if (!entry.wikiUrl.isEmpty()) + { + out += gp::String::format(" See: {}\n", entry.wikiUrl); + } + if (entry.isExpected) + { + out += " (expected - not a bug)\n"; + } + if (entry.isAlwaysFatal) + { + out += " [!] always fatal\n"; + } + } + return out; +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/ErrorSink.cpp b/source/runtime/core/private/errors/ErrorSink.cpp new file mode 100644 index 0000000..389a232 --- /dev/null +++ b/source/runtime/core/private/errors/ErrorSink.cpp @@ -0,0 +1,68 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/ErrorSink.hpp" + +namespace gp::error +{ + +void Sink::flush() +{} + +GP_NODISCARD Severity Sink::minSeverity() const noexcept +{ + return m_minSeverity; +} + +void Sink::setMinSeverity(Severity severity) noexcept +{ + m_minSeverity = severity; +} + +void Sink::addDomainFilter(Domain domain) +{ + m_domainFilter.pushBack(domain); +} + +void Sink::clearDomainFilter() +{ + m_domainFilter.clear(); +} + +GP_NODISCARD const gp::String& Sink::name() const noexcept +{ + return m_name; +} + +void Sink::setName(gp::String name) +{ + m_name = std::move(name); +} + +void Sink::dispatch(const ErrorRecord& record) +{ + if (record.severity < m_minSeverity) + { + return; + } + if (!m_domainFilter.isEmpty()) + { + bool isAllowed = false; + for (Domain domain: m_domainFilter) + { + if (domain == record.code.domain()) + { + isAllowed = true; + break; + } + } + if (!isAllowed) + { + return; + } + } + onRecord(record); +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/ErrorSystem.cpp b/source/runtime/core/private/errors/ErrorSystem.cpp new file mode 100644 index 0000000..608709e --- /dev/null +++ b/source/runtime/core/private/errors/ErrorSystem.cpp @@ -0,0 +1,370 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/ErrorSystem.hpp" +#include "container/BasicString.hpp" // IWYU pragma: keep +#include "container/BasicStringView.hpp" // IWYU pragma: keep +#include "container/Forward.hpp" +#include "CoreMinimal.hpp" +#include "errors/ErrorSeverity.hpp" +#include "errors/sinks/AbortSink.hpp" +#if GP_BUILD_DEBUG + #include "errors/sinks/BreakpointSink.hpp" +#endif +#include "errors/sinks/ConsoleSink.hpp" +#include "errors/sinks/FileSink.hpp" +#include "memory/UniquePtr.hpp" +#if GP_PLATFORM_WINDOWS + #include +#elif GP_PLATFORM_LINUX || GP_PLATFORM_APPLE + #include +#endif +#include +#include +#include + +namespace gp::error +{ + +std::atomic ErrorSystem::s_instance{ nullptr }; +std::mutex ErrorSystem::s_initMutex; +std::mutex ErrorSystem::m_sinkMutex; + +ErrorSystem::ErrorSystem(ErrorSystemConfig config) + : m_config(std::move(config)) + , m_rootSink(gp::makeUnique()) + , m_initTime(std::chrono::steady_clock::now()) +{ + if (m_config.addDefaultConsoleSink) + { + auto consoleSink = std::make_shared(/*useAnsiColor=*/true); + consoleSink->setMinSeverity(m_config.filter.globalMinSeverity); + m_rootSink->addSink(std::move(consoleSink)); + } + + if (!m_config.defaultLogFilePath.isEmpty()) + { + try + { + auto fileSink = std::make_shared(m_config.defaultLogFilePath); + fileSink->setMinSeverity(m_config.filter.globalMinSeverity); + m_rootSink->addSink(std::move(fileSink)); + } + catch (const std::exception& e) + { + std::fprintf(stderr, "[GP ErrorSystem] WARNING: Could not open log file: %s\n", e.what()); + } + } + +#if GP_BUILD_DEBUG + if (m_config.breakpoint.checkIsDebugged) + { + auto breakpointSink = std::make_shared(m_config.breakpoint.breakFrom); + m_rootSink->addSink(std::move(breakpointSink)); + } +#endif + + { + auto abortSink = std::make_shared( + m_config.abort.abortFrom, m_config.abort.useTerminate ? AbortSink::Mode::Terminate : AbortSink::Mode::Abort + ); + m_rootSink->addSink(std::move(abortSink)); + } +} + +ErrorSystem::~ErrorSystem() +{ + m_rootSink->flush(); +} + +void ErrorSystem::initialize(ErrorSystemConfig config) +{ + std::lock_guard lock(s_initMutex); + + if (s_instance.load(std::memory_order_acquire) != nullptr) + { + ErrorSystem::dispatch( + Severity::Warning, + codes::kAlreadyExists, + "ErrorSystem::initialize() called more than once, ignoring.", + std::source_location::current() + ); + return; + } + + auto* instance = new ErrorSystem(std::move(config)); + s_instance.store(instance, std::memory_order_release); + + if (instance->m_config.printBannerOnInit) + { + // TODO: Replace with internal logging once available + std::fprintf( + stderr, + "\033[36m[GP ErrorSystem] Initialized - build: %s %s" " | stacktrace: %s | globalMin: %s\033[0m\n", + __DATE__, + __TIME__, +#if GP_HAS_STACKTRACE + instance->m_config.stacktrace.enabled ? "on" : "off", +#else + "n/a", +#endif + getSeverityDisplay(instance->m_config.filter.globalMinSeverity).data() + ); + } +} + +void ErrorSystem::shutdown() +{ + std::lock_guard lock(s_initMutex); + ErrorSystem* instance = s_instance.exchange(nullptr, std::memory_order_acq_rel); + if (instance) + { + instance->m_rootSink->flush(); + delete instance; + } +} + +bool ErrorSystem::isInitialized() noexcept +{ + return s_instance.load(std::memory_order_acquire) != nullptr; +} + +void ErrorSystem::addSink(std::shared_ptr sink) +{ + std::lock_guard lock(m_sinkMutex); + if (auto* instance = s_instance.load(std::memory_order_acquire)) + { + instance->m_rootSink->addSink(std::move(sink)); + } +} + +void ErrorSystem::removeSink(const gp::String& name) +{ + std::lock_guard lock(m_sinkMutex); + if (auto* instance = s_instance.load(std::memory_order_acquire)) + { + instance->m_rootSink->removeSink(name); + } +} + +void ErrorSystem::clearSinks() +{ + std::lock_guard lock(m_sinkMutex); + if (auto* instance = s_instance.load(std::memory_order_acquire)) + { + instance->m_rootSink = gp::makeUnique(); + } +} + +void ErrorSystem::flushAll() +{ + std::lock_guard lock(m_sinkMutex); + if (auto* instance = s_instance.load(std::memory_order_acquire)) + { + instance->m_rootSink->flush(); + } +} + +GP_NODISCARD MultiSink& ErrorSystem::rootSink() +{ + GP_ASSERT(isInitialized() && "ErrorSystem must be initialized before accessing rootSink"); + std::lock_guard lock(m_sinkMutex); + return *s_instance.load(std::memory_order_acquire)->m_rootSink; +} + +const ErrorSystemConfig& ErrorSystem::config() noexcept +{ + GP_ASSERT(isInitialized() && "ErrorSystem must be initialized before accessing rootSink"); + return s_instance.load(std::memory_order_acquire)->m_config; +} + +void ErrorSystem::setGlobalMinSeverity(Severity severity) noexcept +{ + if (auto* inst = s_instance.load(std::memory_order_acquire)) + { + inst->m_config.filter.globalMinSeverity = severity; + } +} + +const ErrorStatistics& ErrorSystem::stats() noexcept +{ + GP_ASSERT(isInitialized() && "ErrorSystem must be initialized before accessing rootSink"); + return s_instance.load(std::memory_order_acquire)->m_stats; +} + +void ErrorSystem::resetStats() noexcept +{ + if (auto* instance = s_instance.load(std::memory_order_acquire)) + { + for (auto& counter: instance->m_stats.counts) + { + counter.store(0, std::memory_order_relaxed); + } + } +} + +void ErrorSystem::dispatch( + Severity severity, + ErrorCode code, + gp::String message, + std::source_location location, + std::shared_ptr cause +) +{ + ErrorSystem* inst = s_instance.load(std::memory_order_acquire); + if (!inst) + { + // Fallback: write to stderr if system not initialized + std::fprintf( + stderr, + "[GP] [%s] %s (%s:%u)\n", + getSeverityDisplay(severity).data(), + message.cStr(), + location.file_name(), + location.line() + ); + return; + } + inst->dispatchImpl(severity, code, std::move(message), location, std::move(cause)); +} + +void ErrorSystem::dispatchImpl( + Severity severity, + ErrorCode code, + gp::String message, + std::source_location location, + std::shared_ptr cause +) +{ + if (severity < m_config.filter.globalMinSeverity) + { + return; + } + + if (m_config.filter.deduplicate) + { + // std::size_t hashIndex = std::hash{}(message) ^ (std::hash{}(code.raw()) << 17u); + // auto now = std::chrono::steady_clock::now(); + // { + // std::lock_guard lock(m_dedupMutex); + // auto it = m_dedupTable.find(hashIndex); + // if (it != m_dedupTable.end()) + // { + // auto elapsed = std::chrono::duration_cast(now - it->second).count(); + // if (static_cast(elapsed) < m_config.filter.dedupWindowMs) + // { + // m_stats.totalDropped.fetch_add(1, std::memory_order_relaxed); + // return; + // } + // } + // m_dedupTable[hashIndex] = now; + // } + } + + auto record = std::make_shared(); + record->severity = severity; + record->code = code; + record->message = std::move(message); + record->location = location; + record->cause = std::move(cause); + record->engineTime = std::chrono::steady_clock::now(); + record->wallTime = std::chrono::system_clock::now(); + record->threadId = std::this_thread::get_id(); + record->threadName = ErrorContext::current().threadName(); + + { + gp::StringView sub = ErrorContext::current().currentSubsystem(); + if (!sub.isEmpty()) + { + record->subsystem = gp::String(sub); + } + } + + { + // MetaBag ctxMeta = ErrorContext::current().flatten(); + // record->metadata.insert( + // record->metadata.end(), std::make_move_iterator(ctxMeta.begin()), std::make_move_iterator(ctxMeta.end()) + // ); + } + +#if GP_HAS_STACKTRACE + if (m_config.stacktrace.enabled && severity >= m_config.stacktrace.captureFrom) + { + record->stacktrace = std::stacktrace::current(m_config.stacktrace.skipFrames, m_config.stacktrace.maxFrames); + } +#endif + + m_stats.increment(severity); + + m_rootSink->dispatch(*record); + + if (m_config.abort.flushBeforeAbort && severity >= Severity::Fatal) + { + m_rootSink->flush(); + } +} + +bool ErrorSystem::shouldDedup(const DedupKey&) +{ + // Full implementation in dispatchImpl. + return false; +} + +void ErrorSystem::onSignal(int sig) +{ + const char* name = "SIGUNKNOWN"; + switch (sig) + { +#if defined(SIGSEGV) + case SIGSEGV: + name = "SIGSEGV (segmentation fault)"; + break; +#endif +#if defined(SIGABRT) + case SIGABRT: + name = "SIGABRT (abort)"; + break; +#endif +#if defined(SIGFPE) + case SIGFPE: + name = "SIGFPE (floating-point exception)"; + break; +#endif +#if defined(SIGILL) + case SIGILL: + name = "SIGILL (illegal instruction)"; + break; +#endif +#if defined(SIGBUS) + case SIGBUS: + name = "SIGBUS (bus error)"; + break; +#endif + } + + ErrorSystem::dispatch( + Severity::Critical, + codes::kUnknown, + gp::String::format("Fatal signal received: {} ({})", name, sig), + std::source_location::current() + ); + + std::signal(sig, SIG_DFL); + std::raise(sig); +} + +void ErrorSystem::installSignalHandlers() +{ +#if GP_PLATFORM_WINDOWS || GP_PLATFORM_LINUX || GP_PLATFORM_APPLE + std::signal(SIGSEGV, onSignal); + std::signal(SIGABRT, onSignal); + std::signal(SIGFPE, onSignal); + std::signal(SIGILL, onSignal); + #if defined(SIGBUS) + std::signal(SIGBUS, onSignal); + #endif +#endif +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/sinks/AbortSink.cpp b/source/runtime/core/private/errors/sinks/AbortSink.cpp new file mode 100644 index 0000000..67e5d32 --- /dev/null +++ b/source/runtime/core/private/errors/sinks/AbortSink.cpp @@ -0,0 +1,38 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/sinks/AbortSink.hpp" +#include +#include +#include + +namespace gp::error +{ + +AbortSink::AbortSink(Severity abortAt, Mode mode) + : m_abortAt(abortAt) + , m_mode(mode) +{ + setName("AbortSink"); + setMinSeverity(abortAt); +} + +void AbortSink::onRecord(const ErrorRecord& record) +{ + if (record.severity >= m_abortAt) + { + std::fflush(stderr); + switch (m_mode) + { + case Mode::Abort: + std::abort(); + break; + case Mode::Terminate: + std::terminate(); + break; + } + } +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/sinks/BreakpointSink.cpp b/source/runtime/core/private/errors/sinks/BreakpointSink.cpp new file mode 100644 index 0000000..c79a981 --- /dev/null +++ b/source/runtime/core/private/errors/sinks/BreakpointSink.cpp @@ -0,0 +1,25 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/sinks/BreakpointSink.hpp" + +namespace gp::error +{ + +BreakpointSink::BreakpointSink(Severity breakAt) + : m_breakAt(breakAt) +{ + setName("BreakpointSink"); + setMinSeverity(breakAt); +} + +void BreakpointSink::onRecord(const ErrorRecord& record) +{ + if (record.severity >= m_breakAt) + { + GP_DEBUGBREAK(); + } +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/sinks/ConsoleSink.cpp b/source/runtime/core/private/errors/sinks/ConsoleSink.cpp new file mode 100644 index 0000000..37fe43e --- /dev/null +++ b/source/runtime/core/private/errors/sinks/ConsoleSink.cpp @@ -0,0 +1,129 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/sinks/ConsoleSink.hpp" +#include + +namespace gp::error +{ + +ConsoleSink::ConsoleSink(bool useAnsiColor, bool toStdout) + : m_useColor(useAnsiColor) + , m_stream(toStdout ? stdout : stderr) +{ + setName("ConsoleSink"); +} + +void ConsoleSink::onRecord(const ErrorRecord& record) +{ + // Format: [WRN][Window:0x0041] Window creation failed (main.cpp:42) + const bool color = m_useColor; + const auto ansi = color ? getSeverityAnsiColor(record.severity) : ""; + const auto reset = color ? "\033[0m" : ""; + const auto bold = color ? "\033[1m" : ""; + + // Wall clock ISO 8601 + auto tt = std::chrono::system_clock::to_time_t(record.wallTime); + char timebuf[32] = {}; + // Thread-safe strftime equivalent (no gmtime_r on MSVC without CRT ext) +#if defined(_MSC_VER) + struct tm tmval{}; + gmtime_s(&tmval, &tt); + strftime(timebuf, sizeof timebuf, "%T", &tmval); +#else + struct tm tmval{}; + gmtime_r(&tt, &tmval); + strftime(timebuf, sizeof timebuf, "%T", &tmval); +#endif + + gp::String line; + line.reserve(256); + line += ansi; + line += gp::String::format("[{}] ", getSeverityName(record.severity)); + line += reset; + line += gp::String::format("{} ", timebuf); + + if (!record.subsystem.isEmpty()) + { + line += gp::String::format("[{}] ", record.subsystem); + } + + line += bold; + line += record.message; + line += reset; + + // Source location (short form, basename only) + std::string_view file = record.location.file_name(); + if (auto pos = file.rfind('/'); pos != std::string_view::npos) + { + file = file.substr(pos + 1); + } + if (auto pos = file.rfind('\\'); pos != std::string_view::npos) + { + file = file.substr(pos + 1); + } + + line += gp::String::format(" ({}:{})", file, record.location.line()); + + if (!record.threadName.isEmpty()) + { + line += gp::String::format(" [{}]", record.threadName); + } + + line += '\n'; + + // Stacktrace (only for Error and above, configurable) + if (m_printStacktrace && record.severity >= Severity::Error) + { +#if GP_HAS_STACKTRACE + if (!record.stacktrace.empty()) + { + line += gp::String::format("{} Stack trace:\n{}", ansi, reset); + for (const auto& frame: record.stacktrace) + { + line += gp::String::format(" {}\n", std::to_string(frame)); + } + } +#endif + } + + // Cause chain + if (record.hasCause() && m_printCause) + { + const ErrorRecord* cause = record.cause.get(); + int depth = 0; + while (cause) + { + line += gp::String::format( + " {}Caused by [{}]{}: {}\n", ansi, getSeverityName(cause->severity), reset, cause->message + ); + cause = cause->cause.get(); + if (++depth > 8) + { + line += " ... (truncated)\n"; + break; + } + } + } + + std::lock_guard lock(m_mutex); + std::fwrite(line.data(), 1, line.size(), m_stream); +} + +void ConsoleSink::flush() +{ + std::fflush(m_stream); +} + +void ConsoleSink::setPrintStacktrace(bool enabled) noexcept +{ + m_printStacktrace = enabled; +} + +void ConsoleSink::setPrintCause(bool enabled) noexcept +{ + m_printCause = enabled; +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/sinks/FileSink.cpp b/source/runtime/core/private/errors/sinks/FileSink.cpp new file mode 100644 index 0000000..8c960ca --- /dev/null +++ b/source/runtime/core/private/errors/sinks/FileSink.cpp @@ -0,0 +1,79 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/sinks/FileSink.hpp" +#include "errors/ErrorCode.hpp" +#include "errors/ErrorSeverity.hpp" +#include +#include + +namespace gp::error +{ + +FileSink::FileSink(const gp::String& path) + : m_file(path.cStr(), std::ios::app | std::ios::out) +{ + setName("FileSink:" + path); + if (!m_file.is_open()) + { + // TODO: Use a inner exception or error code instead of std::runtime_error + throw std::runtime_error(("GP FileSink: cannot open log file: " + path).cStr()); + } + + m_file << gp::String::format( + "=== GP Engine Log — opened {} ===\n", std::chrono::system_clock::now().time_since_epoch().count() + ); +} + +void FileSink::onRecord(const ErrorRecord& record) +{ + auto tt = std::chrono::system_clock::to_time_t(record.wallTime); + char buf[32] = {}; +#if defined(_MSC_VER) + struct tm tm{}; + gmtime_s(&tm, &tt); + strftime(buf, sizeof buf, "%Y-%m-%d %T", &tm); +#else + struct tm tm{}; + gmtime_r(&tt, &tm); + strftime(buf, sizeof buf, "%Y-%m-%d %T", &tm); +#endif + + std::lock_guard lock(m_mutex); + + m_file << gp::String::format( + "[{}][{}][{}:0x{:04X}] {} | {}:{}\n", + buf, + getSeverityName(record.severity), + getDomainName(record.code.domain()), + record.code.code(), + record.message, + record.location.file_name(), + record.location.line() + ); + + for (const auto& m: record.metadata) + { + m_file << gp::String::format(" meta: {}={}\n", m.key, m.value); + } + +#if GP_HAS_STACKTRACE + if (!record.stacktrace.empty()) + { + m_file << " stacktrace:\n"; + for (const auto& frame: record.stacktrace) + { + m_file << gp::String::format(" {}\n", std::to_string(frame)); + } + } +#endif +} + +void FileSink::flush() +{ + std::lock_guard lock(m_mutex); + m_file.flush(); +} + +} // namespace gp::error diff --git a/source/runtime/core/private/errors/sinks/MultiSink.cpp b/source/runtime/core/private/errors/sinks/MultiSink.cpp new file mode 100644 index 0000000..e661c42 --- /dev/null +++ b/source/runtime/core/private/errors/sinks/MultiSink.cpp @@ -0,0 +1,72 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#include "errors/sinks/MultiSink.hpp" + +namespace gp::error +{ + +MultiSink::MultiSink() +{ + setName("MultiSink"); +} + +void MultiSink::addSink(std::shared_ptr sink) +{ + if (!sink) + { + return; + } + + std::lock_guard lock(m_mutex); + m_sinks.pushBack(std::move(sink)); +} + +void MultiSink::removeSink(const gp::String& name) +{ + std::lock_guard lock(m_mutex); + gp::eraseIf( + m_sinks, + [&](const auto& sink) + { + return sink->name() == name; + } + ); +} + +GP_NODISCARD gp::USize MultiSink::sinkCount() const noexcept +{ + std::lock_guard lock(m_mutex); + return m_sinks.size(); +} + +void MultiSink::onRecord(const ErrorRecord& record) +{ + decltype(m_sinks) sinks; + { + std::lock_guard lock(m_mutex); + sinks = m_sinks; + } + + for (auto& sink: sinks) + { + sink->dispatch(record); + } +} + +void MultiSink::flush() +{ + decltype(m_sinks) sinks; + { + std::lock_guard lock(m_mutex); + sinks = m_sinks; + } + + for (auto& sink: sinks) + { + sink->flush(); + } +} + +} // namespace gp::error diff --git a/source/runtime/core/public/container/Array.hpp b/source/runtime/core/public/container/Array.hpp index 132ae6e..e9b7d8f 100644 --- a/source/runtime/core/public/container/Array.hpp +++ b/source/runtime/core/public/container/Array.hpp @@ -372,9 +372,9 @@ class Array auto it = std::ranges::find_if_not( m_data, [&value](const T& elem) - { - return elem == value; - } + { + return elem == value; + } ); return it != end() ? static_cast(it - begin()) : npos; } @@ -388,9 +388,9 @@ class Array auto it = std::ranges::find_if_not( std::views::reverse(m_data), [&value](const T& elem) - { - return elem == value; - } + { + return elem == value; + } ); return it != std::views::reverse(m_data).end() ? static_cast(it.base() - begin() - 1) : npos; } diff --git a/source/runtime/core/public/container/BasicString.hpp b/source/runtime/core/public/container/BasicString.hpp index e472cb8..a3cdddf 100644 --- a/source/runtime/core/public/container/BasicString.hpp +++ b/source/runtime/core/public/container/BasicString.hpp @@ -10,9 +10,11 @@ #include "math/LinearAlgebra.hpp" #include "memory/AllocatorTraits.hpp" #include +#include #include #include #include +#include namespace gp { @@ -53,6 +55,9 @@ class BasicString using ReverseIterator = std::reverse_iterator; using ConstReverseIterator = std::reverse_iterator; + // std::string compatibility typedefs + using value_type = CharT; + static constexpr SizeType npos = static_cast(-1); private: @@ -1340,6 +1345,13 @@ class BasicString return _view().contains(str); } + /// @brief Returns a string_view of this string. + /// @return A BasicStringView representing the contents of this string. + GP_NODISCARD BasicStringView asView() const noexcept + { + return _view(); + } + /// @brief Returns a copy of the allocator. /// @return The allocator used by this string. GP_NODISCARD AllocatorType getAllocator() const noexcept @@ -1547,6 +1559,28 @@ class BasicString m_allocator.~AllocatorType(); ::new (static_cast(&m_allocator)) AllocatorType(static_cast(newAlloc)); } + +public: + /// @brief Formats a string using std::format syntax and returns it as a gp::String. + /// @details + /// Pre-computes the required buffer size via std::formatted_size, performs exactly + /// one allocation via resize(), then writes directly to the buffer via a raw-pointer + /// output iterator. The result is always null-terminated. + /// This avoids the need for a push_back-compatible back_inserter on gp::String. + /// @tparam Args Variadic template parameters for the format arguments. + /// @param[in] format The format string, following std::format syntax. + /// @param[in] args The arguments to format. + /// @return The formatted string as a gp::String. + template + static BasicString format(std::format_string format, Args&&... args) + { + // TODO: use std::basic_format_string in order to support wide-character strings without code duplication. + BasicString result; + const size_t requiredSize = std::formatted_size(format, std::forward(args)...); + result.resize(requiredSize); + std::format_to(result.data(), format, std::forward(args)...); + return result; + } }; static_assert(sizeof(BasicString) == 32, "gp::string must be exactly 32 bytes"); @@ -1625,3 +1659,17 @@ std::ostream& operator<<(std::ostream& os, const gp::BasicString to gain all standard string format specifiers. +template +struct std::formatter, CharT> + : std::formatter, CharT> +{ + template + auto format(const gp::BasicString& str, FormatContext& ctx) const + { + std::basic_string_view sv(str.data(), str.size()); + return std::formatter, CharT>::format(sv, ctx); + } +}; diff --git a/source/runtime/core/public/container/BasicStringView.hpp b/source/runtime/core/public/container/BasicStringView.hpp index ad20386..72a8a2e 100644 --- a/source/runtime/core/public/container/BasicStringView.hpp +++ b/source/runtime/core/public/container/BasicStringView.hpp @@ -8,7 +8,10 @@ #include "CoreMinimal.hpp" #include "math/LinearAlgebra.hpp" #include +#include +#include #include +#include namespace gp { @@ -797,3 +800,15 @@ std::ostream& operator<<(std::ostream& os, const gp::BasicStringView +struct std::formatter, CharT> : std::formatter, CharT> +{ + template + auto format(gp::BasicStringView sv, FormatContext& ctx) const + { + std::basic_string_view std_sv(sv.data(), sv.size()); + return std::formatter, CharT>::format(std_sv, ctx); + } +}; diff --git a/source/runtime/core/public/container/Vector.hpp b/source/runtime/core/public/container/Vector.hpp index fc78c61..d6d72e2 100644 --- a/source/runtime/core/public/container/Vector.hpp +++ b/source/runtime/core/public/container/Vector.hpp @@ -8,6 +8,7 @@ #include "CoreMinimal.hpp" #include "memory/AllocatorTraits.hpp" #include "memory/Forward.hpp" +#include #include #include #include @@ -1113,4 +1114,32 @@ class Vector } }; +/// @brief Erases all elements that satisfy the predicate from gp::Vector. +/// @tparam Predicate The type of the predicate function or function object. +/// @param[in,out] vec The vector to erase elements from. +/// @param[in] pred The predicate function or function object that returns true for elements to erase +/// @return The number of erased elements. +template +constexpr typename Vector::SizeType eraseIf(Vector& vec, Predicate pred) +{ + auto it = std::remove_if(vec.begin(), vec.end(), pred); + auto r = static_cast::SizeType>(std::distance(it, vec.end())); + vec.erase(it, vec.end()); + return r; +} + +/// @brief Erases all elements that are equal to value from gp::Vector. +/// @tparam U The type of the value to compare against (can be different from T if T supports heterogeneous equality). +/// @param[in,out] vec The vector to erase elements from. +/// @param[in] value The value to compare against for erasure. +/// @return The number of erased elements. +template +constexpr typename Vector::SizeType erase(Vector& vec, const U& value) +{ + auto it = std::remove(vec.begin(), vec.end(), value); + auto r = static_cast::SizeType>(std::distance(it, vec.end())); + vec.erase(it, vec.end()); + return r; +} + } // namespace gp diff --git a/source/runtime/core/public/errors/Error.hpp b/source/runtime/core/public/errors/Error.hpp new file mode 100644 index 0000000..c56c36e --- /dev/null +++ b/source/runtime/core/public/errors/Error.hpp @@ -0,0 +1,153 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "container/BasicString.hpp" // IWYU pragma: keep +#include "container/Forward.hpp" +#include "CoreMinimal.hpp" +#include "errors/ErrorCode.hpp" +#include "errors/ErrorRecord.hpp" +#include "errors/ErrorSeverity.hpp" +#include "errors/ErrorSystem.hpp" +#include + +namespace gp::error +{ + +/// @brief Raises an error with the specified severity, code, message, cause, and source location. +/// @param[in] severity The severity level of the error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] code The specific error code representing the type of error. +/// @param[in] message A descriptive message providing details about the error. +/// @param[in] cause An optional shared pointer to an ErrorRecord representing the cause of this error (can be nullptr). +/// @param[in] location The source location where the error was raised (defaults to the current location if not +/// provided). +inline void raise( + Severity severity, + ErrorCode code, + gp::String message, + std::shared_ptr cause = nullptr, + std::source_location location = std::source_location::current() +) +{ + ErrorSystem::dispatch(severity, code, std::move(message), location, std::move(cause)); +} + +/// @brief Raises an error with the specified severity and message. +/// @param[in] severity The severity level of the error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] message A descriptive message providing details about the error. +/// @param[in] location The source location where the error was raised (defaults to the current location if not +/// provided). +inline void + raise(Severity severity, gp::String message, std::source_location location = std::source_location::current()) +{ + ErrorSystem::dispatch(severity, codes::kUnknown, std::move(message), location, nullptr); +} + +/// @brief Wraps an existing error record with additional context and raises a new error. +/// @param[in] inner The existing ErrorRecord to be wrapped as the cause of the new error. +/// @param[in] severity The severity level of the new error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] code The specific error code representing the type of the new error. +/// @param[in] message A descriptive message providing details about the new error. +/// @param[in] location The source location where the new error was raised (defaults to the current location if not +/// provided). +inline void wrap( + const ErrorRecord& inner, + Severity severity, + ErrorCode code, + gp::String message, + std::source_location location = std::source_location::current() +) +{ + ErrorSystem::dispatch(severity, code, std::move(message), location, std::make_shared(inner)); +} + +} // namespace gp::error + +namespace gp +{ + +/// @brief Creates an ErrorRecord with the specified severity, code, message, and source location. +/// @param[in] severity The severity level of the error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] code The specific error code representing the type of error. +/// @param[in] message A descriptive message providing details about the error. +/// @param[in] location The source location where the error was created (defaults to the current location if not +/// provided). +/// @return An ErrorRecord containing the provided information and additional metadata such as timestamps and thread +/// info. +GP_NODISCARD inline error::ErrorRecord makeError( + error::Severity severity, + error::ErrorCode code, + gp::String message, + std::source_location location = std::source_location::current() +) +{ + error::ErrorRecord r; + r.severity = severity; + r.code = code; + r.message = std::move(message); + r.location = location; + r.wallTime = std::chrono::system_clock::now(); + r.engineTime = std::chrono::steady_clock::now(); + r.threadId = std::this_thread::get_id(); + r.threadName = error::ErrorContext::current().threadName(); + return r; +} + +} // namespace gp + +/// @brief Macro to raise an error with the specified severity, code, and message, automatically capturing the source +/// location. +/// @param[in] sev_ The severity level of the error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] code_ The specific error code representing the type of error. +/// @param[in] msg_ A descriptive message providing details about the error. +#define GP_DETAIL_RAISE(sev_, code_, msg_) \ + ::gp::error::ErrorSystem::dispatch((sev_),(code_),(msg_),std::source_location::current()) + +/// @brief Macro to raise an error with a specific severity and code, using the GP_DETAIL_RAISE macro. +/// @param[in] sev_ The severity level of the error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] code_ The specific error code representing the type of error. +/// @param[in] msg_ A descriptive message providing details about the error. +#define GP_RAISE_CODE(sev_, code_, msg_) GP_DETAIL_RAISE((sev_),(code_),(msg_)) + +/// @brief Macro to raise an error with a specific severity and message, using the GP_DETAIL_RAISE macro with an unknown +/// error code. +/// @param[in] sev_ The severity level of the error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] msg_ A descriptive message providing details about the error. +#define GP_RAISE(sev_, msg_) GP_DETAIL_RAISE((sev_),::gp::error::codes::kUnknown,(msg_)) + +/// @brief Macro to raise a formatted error message with a specific severity and code, using the GP_DETAIL_RAISE macro. +/// @param[in] sev_ The severity level of the error (e.g., trace, debug, info, warning, error, fatal). +/// @param[in] code_ The specific error code representing the type of error. +/// @param[in] fmt_ A format string for the error message. +/// @param[in] ... Additional arguments to be formatted into the error message. +#define GP_RAISE_FMT(sev_, code_, fmt_, ...) GP_DETAIL_RAISE((sev_),(code_),gp::String::format((fmt_),__VA_ARGS__)) + +/// @brief Macro to raise a trace-level error with a specific message. +/// @param[in] m_ A descriptive message providing details about the trace event. +#define GP_TRACE(m_) GP_RAISE(::gp::error::trace, (m_)) + +/// @brief Macro to raise a debug-level error with a specific message. +/// @param[in] m_ A descriptive message providing details about the debug event. +#define GP_DEBUG(m_) GP_RAISE(::gp::error::debug,(m_)) + +/// @brief Macro to raise an info-level error with a specific message. +/// @param[in] m_ A descriptive message providing details about the info event. +#define GP_INFO(m_) GP_RAISE(::gp::error::info,(m_)) + +/// @brief Macro to raise a warning-level error with a specific message. +/// @param[in] m_ A descriptive message providing details about the warning event. +#define GP_WARN(m_) GP_RAISE(::gp::error::warning,(m_)) + +/// @brief Macro to raise an error-level error with a specific message. +/// @param[in] m_ A descriptive message providing details about the error event. +#define GP_ERROR(m_) GP_RAISE(::gp::error::error,(m_)) + +/// @brief Macro to raise a fatal-level error with a specific message. +/// @param[in] m_ A descriptive message providing details about the fatal event. +#define GP_FATAL(m_) GP_RAISE(::gp::error::fatal,(m_)) + +/// @brief Macro to raise a critical-level error (panic) with a specific message. +/// @param[in] m_ A descriptive message providing details about the critical event. +#define GP_PANIC(m_) GP_RAISE(::gp::error::critical,(m_)) diff --git a/source/runtime/core/public/errors/Config.hpp b/source/runtime/core/public/errors/ErrorConfig.hpp similarity index 94% rename from source/runtime/core/public/errors/Config.hpp rename to source/runtime/core/public/errors/ErrorConfig.hpp index f8527d9..26eabd1 100644 --- a/source/runtime/core/public/errors/Config.hpp +++ b/source/runtime/core/public/errors/ErrorConfig.hpp @@ -7,7 +7,7 @@ #include "container/BasicString.hpp" // IWYU pragma: keep #include "container/Forward.hpp" #include "CoreMinimal.hpp" -#include "errors/Severity.hpp" +#include "errors/ErrorSeverity.hpp" namespace gp::error { @@ -95,7 +95,7 @@ struct ErrorSystemConfig public: /// @brief Factory method for a preset development configuration (all features on). /// @return An ErrorSystemConfig instance with development settings. - GP_NODISCARD static constexpr ErrorSystemConfig getDevelopmentConfig() noexcept + GP_NODISCARD static inline ErrorSystemConfig getDevelopmentConfig() noexcept { ErrorSystemConfig config; config.stacktrace.captureFrom = Severity::Warning; @@ -108,7 +108,7 @@ struct ErrorSystemConfig /// @brief Factory method for a preset staging configuration (some features on, some off). /// @return An ErrorSystemConfig instance with staging settings. - GP_NODISCARD static constexpr ErrorSystemConfig getStagingConfig() noexcept + GP_NODISCARD static inline ErrorSystemConfig getStagingConfig() noexcept { ErrorSystemConfig config; config.stacktrace.captureFrom = Severity::Error; @@ -122,7 +122,7 @@ struct ErrorSystemConfig /// @brief Factory method for a preset shipping configuration (most features off for performance). /// @return An ErrorSystemConfig instance with shipping settings. - GP_NODISCARD static constexpr ErrorSystemConfig getShippingConfig() noexcept + GP_NODISCARD static inline ErrorSystemConfig getShippingConfig() noexcept { ErrorSystemConfig config; config.stacktrace.enabled = false; // No stack traces @@ -137,7 +137,7 @@ struct ErrorSystemConfig /// @brief Factory method for a preset test configuration (strict settings for testing). /// @return An ErrorSystemConfig instance with test settings. - GP_NODISCARD static constexpr ErrorSystemConfig getTestConfig() noexcept + GP_NODISCARD static inline ErrorSystemConfig getTestConfig() noexcept { ErrorSystemConfig config; config.stacktrace.captureFrom = Severity::Fatal; diff --git a/source/runtime/core/public/errors/ErrorContext.hpp b/source/runtime/core/public/errors/ErrorContext.hpp new file mode 100644 index 0000000..4713190 --- /dev/null +++ b/source/runtime/core/public/errors/ErrorContext.hpp @@ -0,0 +1,145 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "container/BasicString.hpp" // IWYU pragma: keep +#include "container/BasicStringView.hpp" // IWYU pragma: keep +#include "container/Forward.hpp" +#include "container/Vector.hpp" +#include "CoreMinimal.hpp" +#include "errors/ErrorRecord.hpp" + +namespace gp::error +{ + +/// @brief Represents a single frame in the error context stack. +struct ContextFrame +{ + gp::String subsystem; // m_stack; + gp::String m_threadName; + +private: + /// @brief Private constructor to enforce singleton pattern. Use ErrorContext::current() to access the instance. + ErrorContext() = default; + +public: + /// @brief Delete copy constructor and assignment operator to enforce singleton pattern. + ErrorContext(const ErrorContext&) = delete; + ErrorContext& operator=(const ErrorContext&) = delete; + +public: + /// @brief Push a new frame onto the stack, describing the current subsystem and operation. + /// @param[in] subsystem Typically a high-level module name, e.g. "Renderer", "Audio" + /// @param[in] operation Typically a specific action, e.g. "LoadTexture", "OpenDevice" + void push(gp::String subsystem, gp::String operation); + + /// @brief Pop the most recent frame from the stack. No-op if stack is empty. + void pop() noexcept; + + /// @brief Attach a key/value tag to the *top* frame. No-op if stack is empty. + /// @param[in] key A short identifier, e.g. "filename", "error_code" + /// @param[in] value A string representation of the value, e.g. "assets/texture.png", "0xDEADBEEF" + void tag(gp::String key, gp::String value); + + /// @brief Check if the stack is empty (i.e. no context frames). + /// @return True if the stack is empty, false otherwise. + GP_NODISCARD bool isEmpty() const noexcept; + + /// @brief Get the current depth of the stack (i.e. number of context frames). + /// @return The number of frames currently on the stack. + GP_NODISCARD gp::USize depth() const noexcept; + + /// @brief Get a const reference to the current stack of context frames. + /// @return A const reference to the vector of context frames. May be empty if no frames have been pushed. + GP_NODISCARD const gp::Vector& frames() const noexcept; + + /// @brief Get the subsystem of the current (top) frame, or an empty string if the stack is empty. + /// @return The subsystem of the current frame, or an empty string if the stack is empty. + GP_NODISCARD gp::StringView currentSubsystem() const noexcept; + + /// @brief Get the flattened context as a MetaBag, suitable for attaching to an ErrorRecord. This will include all + /// frames and tags, with keys formatted as "scope[i].subsystem", "scope[i].operation", and "scope[i].tagkey". + /// @return A MetaBag containing all context information from the stack, flattened into key/value pairs. + GP_NODISCARD MetaBag flatten() const; + + /// @brief Set the name of the current thread (for debugging purposes). + /// @note + /// This is purely informational and does not affect thread behavior. + /// It can be used to make error reports more readable by indicating which thread they came from. + /// @param[in] name A human-readable name for the thread, e.g. "MainThread", "WorkerThread1". + void setThreadName(gp::String name); + + /// @brief Get the name of the current thread. + /// @return The name of the current thread, or an empty string if no name has been set. + GP_NODISCARD const gp::String& threadName() const noexcept; + +public: + /// @brief Get the current error context instance. + /// @note This is a thread-local singleton; each thread has its own independent context stack. + /// @return A reference to the current error context instance. + static ErrorContext& current() noexcept + { + thread_local ErrorContext instance; + return instance; + } +}; + +/// @brief RAII guard that pushes a new context frame on construction and pops it on destruction. +/// This is intended to be used with the GP_ERROR_SCOPE macro for convenient scoping of error contexts. +class ContextScope +{ +public: + /// @brief Construct a new ContextScope, pushing a new frame onto the error context stack. The frame will be popped + /// automatically when this object is destroyed (e.g. at the end of the enclosing block). + /// @param[in] subsystem The subsystem name for this scope, e.g. "Renderer", "Audio" + /// @param[in] operation The operation name for this scope, e.g. "LoadTexture", "OpenDevice" + explicit ContextScope(gp::String subsystem, gp::String operation); + + /// @brief Destructor that pops the context frame from the stack. + ~ContextScope() noexcept; + + // Non-copyable, non-movable (RAII identity must be stable) + ContextScope(const ContextScope&) = delete; + ContextScope& operator=(const ContextScope&) = delete; + ContextScope(ContextScope&&) = delete; + ContextScope& operator=(ContextScope&&) = delete; + +public: + /// @brief Attach a key/value tag to this scope. Chainable. + /// @param[in] key A short identifier for the tag, e.g. "filename", "error_code" + /// @param[in] value A string representation of the value, e.g. "assets/texture.png", "0xDEADBEEF" + /// @return A reference to this ContextScope, allowing for chaining multiple tags. + ContextScope& tag(gp::String key, gp::String value); +}; + +} // namespace gp::error + +/// @brief Create a new error scope with the given subsystem and operation. +/// @param[in] subsystem The subsystem name for this scope, e.g. "Renderer", "Audio" +/// @param[in] operation The operation name for this scope, e.g. "LoadTexture", "OpenDevice", etc. +#define GP_ERROR_SCOPE(subsystem_, op_) \ + ::gp::error::ContextScope GP_CONCAT(gp_err_scope_, __LINE__){ (subsystem_), (op_) } + +/// @brief Attach a key/value tag to the innermost active scope on this thread. +/// @param[in] key A short identifier for the tag, e.g. "filename", "error_code" +/// @param[in] value A string representation of the value, e.g. "assets/texture.png", "0xDEADBEEF", etc. +#define GP_ERROR_TAG(key_, value_) \ + ::gp::error::ErrorContext::current().tag((key_), gp::String::format("{}", (value_))) + +/// @brief Set the name of the current thread (for debugging purposes). +/// @param[in] name A human-readable name for the thread, e.g. "MainThread", "WorkerThread1", etc. +#define GP_THREAD_NAME(name_) \ + ::gp::error::ErrorContext::current().setThreadName(name_) diff --git a/source/runtime/core/public/errors/ErrorFilter.hpp b/source/runtime/core/public/errors/ErrorFilter.hpp new file mode 100644 index 0000000..f076f4e --- /dev/null +++ b/source/runtime/core/public/errors/ErrorFilter.hpp @@ -0,0 +1,334 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "container/BasicString.hpp" // IWYU pragma: keep +#include "container/BasicStringView.hpp" // IWYU pragma: keep +#include "container/Forward.hpp" +#include "CoreMinimal.hpp" +#include "errors/ErrorRecord.hpp" +#include "errors/ErrorRegistry.hpp" +#include "errors/ErrorSeverity.hpp" +#include // TODO: gp::Atomic? +#include // TODO: gp::TimePoint?, gp::Duration?, etc. +#include // TODO: gp::Predicate? gp::Function? +#include // TODO: gp::SharedPtr? +#include // TODO: gp::Regex? + +namespace gp::error +{ + +/// @brief A value-semantic, composable predicate for filtering ErrorRecords. +class FilterPredicate +{ +public: + using FunctionType = std::function; + +private: + FunctionType m_func; + +public: + /// @brief Construct a FilterPredicate from any callable with the appropriate signature. + /// @param[in] func A callable that takes a const ErrorRecord& and returns bool. + explicit FilterPredicate(FunctionType func) + : m_func(std::move(func)) + {} + +public: + /// @brief Evaluate the predicate on a given ErrorRecord. + /// @param[in] record The ErrorRecord to evaluate. + /// @return true if the record matches the predicate, false otherwise. + GP_NODISCARD bool operator()(const ErrorRecord& record) const + { + return m_func(record); + } + + /// @brief Combine this predicate with another using logical AND. + /// @param[in] other Another FilterPredicate to combine with. + /// @return A new FilterPredicate that represents the logical AND of this and the other predicate. + GP_NODISCARD FilterPredicate operator&&(const FilterPredicate& other) const + { + auto lhs = m_func, rhs = other.m_func; + return FilterPredicate{ [lhs, rhs](const ErrorRecord& record) + { + return lhs(record) && rhs(record); + } }; + } + + /// @brief Combine this predicate with another using logical OR. + /// @param[in] other Another FilterPredicate to combine with. + /// @return A new FilterPredicate that represents the logical OR of this and the other predicate. + GP_NODISCARD FilterPredicate operator||(const FilterPredicate& other) const + { + auto lhs = m_func, rhs = other.m_func; + return FilterPredicate{ [lhs, rhs](const ErrorRecord& record) + { + return lhs(record) || rhs(record); + } }; + } + + /// @brief Negate this predicate. + /// @return A new FilterPredicate that represents the logical NOT of this predicate. + GP_NODISCARD FilterPredicate operator!() const + { + auto lhs = m_func; + return FilterPredicate{ [lhs](const ErrorRecord& record) + { + return !lhs(record); + } }; + } +}; + +namespace filters +{ + +/// @brief Predicate that returns true for records with severity at least @p severity. +/// @param[in] severity The minimum severity level to match. +/// @return A FilterPredicate that matches records with severity >= severity. +GP_NODISCARD inline FilterPredicate atLeast(Severity severity) +{ + return FilterPredicate{ [severity](const ErrorRecord& record) + { + return record.severity >= severity; + } }; +} + +/// @brief Predicate that returns true for records with severity at most @p severity. +/// @param[in] severity The maximum severity level to match. +/// @return A FilterPredicate that matches records with severity <= severity. +GP_NODISCARD inline FilterPredicate atMost(Severity severity) +{ + return FilterPredicate{ [severity](const ErrorRecord& record) + { + return record.severity <= severity; + } }; +} + +/// @brief Predicate that returns true for records with severity exactly @p severity. +/// @param[in] severity The severity level to match. +/// @return A FilterPredicate that matches records with severity == severity. +GP_NODISCARD inline FilterPredicate exactly(Severity severity) +{ + return FilterPredicate{ [severity](const ErrorRecord& record) + { + return record.severity == severity; + } }; +} + +/// @brief Predicate that returns true for records with severity in the range [minimumSeverity, maximumSeverity]. +/// @param[in] minimumSeverity The minimum severity level to match (inclusive). +/// @param[in] maximumSeverity The maximum severity level to match (inclusive). +/// @return A FilterPredicate that matches records with severity in the specified range. +GP_NODISCARD inline FilterPredicate severityRange(Severity minimumSeverity, Severity maximumSeverity) +{ + return FilterPredicate{ [minimumSeverity, maximumSeverity](const ErrorRecord& record) + { + return record.severity >= minimumSeverity && record.severity <= maximumSeverity; + } }; +} + +/// @brief Predicate that returns true for records with a specific domain. +/// @param[in] domain The domain to match. +/// @return A FilterPredicate that matches records with the specified domain. +GP_NODISCARD inline FilterPredicate domain(Domain domain) +{ + return FilterPredicate{ [domain](const ErrorRecord& record) + { + return record.code.domain() == domain; + } }; +} + +/// @brief Predicate that returns true for records with a specific error code. +/// @param[in] code The error code to match. +/// @return A FilterPredicate that matches records with the specified error code. +GP_NODISCARD inline FilterPredicate code(ErrorCode code) +{ + return FilterPredicate{ [code](const ErrorRecord& record) + { + return record.code == code; + } }; +} + +/// @brief Predicate that returns true for records with any of the specified error codes. +/// @param[in] codes An initializer list of error codes to match. +/// @return A FilterPredicate that matches records with any of the specified error codes. +GP_NODISCARD inline FilterPredicate anyCode(std::initializer_list codes) +{ + std::vector v{ codes }; + return FilterPredicate{ [v](const ErrorRecord& record) + { + for (const auto& c: v) + { + if (record.code == c) + { + return true; + } + } + return false; + } }; +} + +/// @brief Predicate that returns true for records originating from a specific subsystem. +/// @param[in] name The name of the subsystem to match. +/// @return A FilterPredicate that matches records from the specified subsystem. +GP_NODISCARD inline FilterPredicate subsystem(gp::String name) +{ + return FilterPredicate{ [name](const ErrorRecord& record) + { + return record.subsystem == name; + } }; +} + +/// @brief Predicate that returns true for records originating from a specific thread. +/// @param[in] name The name of the thread to match. +/// @return A FilterPredicate that matches records from the specified thread. +GP_NODISCARD inline FilterPredicate thread(gp::String name) +{ + return FilterPredicate{ [name](const ErrorRecord& record) + { + return record.threadName == name; + } }; +} + +/// @brief Predicate that returns true for records whose message contains a specific substring. +/// @param[in] needle The substring to search for in the message. +/// @return A FilterPredicate that matches records whose message contains the specified substring. +GP_NODISCARD inline FilterPredicate messageContains(gp::String needle) +{ + return FilterPredicate{ [needle](const ErrorRecord& record) + { + return record.message.find(needle) != gp::String::npos; + } }; +} + +/// @brief Predicate that returns true for records whose message matches a regex pattern. +/// @param[in] pattern The regex pattern to match against the message. +/// @return A FilterPredicate that matches records whose message matches the specified regex pattern. +GP_NODISCARD inline FilterPredicate messageMatches(const gp::String& pattern) +{ + auto re = std::make_shared(pattern.cStr()); + return FilterPredicate{ [re](const ErrorRecord& record) + { + return std::regex_search(record.message.cStr(), *re); + } }; +} + +/// @brief Predicate that returns true for records originating from a source file whose path contains a specific +/// substring. +/// @param[in] path The substring to search for in the source file path. +/// @return A FilterPredicate that matches records originating from source files whose path contains the specified +/// substring. +GP_NODISCARD inline FilterPredicate sourceFile(gp::String path) +{ + return FilterPredicate{ [path](const ErrorRecord& record) + { + return gp::StringView{ record.location.file_name() }.find(path) != gp::StringView::npos; + } }; +} + +/// @brief Predicate that returns true for records that have a specific metadata key-value pair. +/// @param[in] key The metadata key to check for. +/// @param[in] value The metadata value to match for the specified key. +/// @return A FilterPredicate that matches records that have the specified metadata key-value pair. +GP_NODISCARD inline FilterPredicate hasMeta(gp::String key, gp::String value) +{ + return FilterPredicate{ [key, value](const ErrorRecord& r) + { + return r.getMetadata(key) == value; + } }; +} + +/// @brief Predicate that returns true for records that have a specific metadata key, regardless of its value. +/// @param[in] key The metadata key to check for. +/// @return A FilterPredicate that matches records that have the specified metadata key. +GP_NODISCARD inline FilterPredicate hasMetaKey(gp::String key) +{ + return FilterPredicate{ [key](const ErrorRecord& record) + { + return !record.getMetadata(key).isEmpty(); + } }; +} + +/// @brief Predicate that returns true for records raised after a specific time point (wall clock). +/// @param[in] when The time point to compare against (records raised after this time will match). +/// @return A FilterPredicate that matches records raised after the specified time point. +GP_NODISCARD inline FilterPredicate after(std::chrono::system_clock::time_point when) +{ + return FilterPredicate{ [when](const ErrorRecord& record) + { + return record.wallTime > when; + } }; +} + +/// @brief Predicate that returns true for records raised before a specific time point (wall clock). +/// @param[in] when The time point to compare against (records raised before this time will match). +/// @return A FilterPredicate that matches records raised before the specified time point. +GP_NODISCARD inline FilterPredicate before(std::chrono::system_clock::time_point when) +{ + return FilterPredicate{ [when](const ErrorRecord& record) + { + return record.wallTime < when; + } }; +} + +/// @brief Predicate that matches all records (always returns true). +/// @return A FilterPredicate that matches all records. +GP_NODISCARD inline FilterPredicate acceptAll() +{ + return FilterPredicate{ [](const ErrorRecord&) + { + return true; + } }; +} + +/// @brief Predicate that matches no records (always returns false). +/// @return A FilterPredicate that matches no records. +GP_NODISCARD inline FilterPredicate rejectAll() +{ + return FilterPredicate{ [](const ErrorRecord&) + { + return false; + } }; +} + +/// @brief Predicate that returns true for records that have a non-empty cause (i.e., were raised with a cause). +/// @return A FilterPredicate that matches records that have a cause. +GP_NODISCARD inline FilterPredicate hasCause() +{ + return FilterPredicate{ [](const ErrorRecord& record) + { + return record.hasCause(); + } }; +} + +/// @brief Predicate that returns true for records that are marked as expected in the registry. +/// @return A FilterPredicate that matches records that are expected according to the registry. +GP_NODISCARD inline FilterPredicate isExpected() +{ + return FilterPredicate{ [](const ErrorRecord& record) + { + return ErrorRegistry::instance().isExpected(record.code); + } }; +} + +/// @brief Predicate that returns true for a random sample of records, with a sampling rate of 1 in N. +/// @param[in] oneInN The sampling rate, where 1 in N records will be accepted on average. +/// @return A FilterPredicate that matches a random sample of records at the specified rate. +GP_NODISCARD inline FilterPredicate sample(gp::UInt32 oneInN) +{ + auto counter = std::make_shared>(0u); + return FilterPredicate{ [counter, oneInN](const ErrorRecord&) + { + if (oneInN == 0) + { + return true; // Avoid division by zero, treat as accept all + } + return counter->fetch_add(1, std::memory_order_relaxed) % oneInN == 0; + } }; +} + +} // namespace filters + +} // namespace gp::error diff --git a/source/runtime/core/public/errors/ErrorRecord.hpp b/source/runtime/core/public/errors/ErrorRecord.hpp new file mode 100644 index 0000000..8b1c991 --- /dev/null +++ b/source/runtime/core/public/errors/ErrorRecord.hpp @@ -0,0 +1,109 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "container/BasicString.hpp" // IWYU pragma: keep +#include "container/BasicStringView.hpp" // IWYU pragma: keep +#include "container/Forward.hpp" +#include "container/Vector.hpp" +#include "CoreMinimal.hpp" // IWYU pragma: keep +#include "errors/ErrorCode.hpp" +#include "errors/ErrorSeverity.hpp" +#include // TODO: gp::TimePoint?, gp::Duration?, etc. +#include // TODO: gp::SharedPtr? +#include +#include // TODO: gp::Thread? + +// clang-format off +#if defined(__cpp_lib_stacktrace) && __cpp_lib_stacktrace >= 202011L + #include + #define GP_HAS_STACKTRACE 1 + using GpStacktrace = std::stacktrace; +#else + #define GP_HAS_STACKTRACE 0 + struct GpStacktrace + { + GP_NODISCARD static GpStacktrace current(std::size_t = 0, std::size_t = 0) { return {}; } + GP_NODISCARD bool empty() const { return true; } + GP_NODISCARD std::size_t size() const { return 0; } + }; +#endif +// clang-format on + +namespace gp::error +{ + +/// @brief A typed key/value pair attached to an ErrorRecord. +/// Stored inline (small-string optimized) to avoid heap allocations on the hot path when only a handful of tags are +/// attached. +struct MetaEntry +{ + gp::String key; + gp::String value; +}; + +/// @brief A vector of metadata entries attached to an ErrorRecord. +using MetaBag = gp::Vector; + +/// @brief The canonical error snapshot. +class ErrorRecord +{ +public: + Severity severity{ Severity::Error }; + ErrorCode code{ ErrorCode::ok() }; + + // Pre-formatted message string. + gp::String message; + // Optional subsystem tag ("RHI", "Audio"…). + gp::String subsystem; + + // Source location (zero overhead in release, inlined by the compiler) + std::source_location location{ std::source_location::current() }; + + // Empty when tracing is disabled. + GpStacktrace stacktrace; + + std::thread::id threadId{ std::this_thread::get_id() }; + gp::String threadName; + + // Engine uptime tick. + std::chrono::steady_clock::time_point engineTime{ std::chrono::steady_clock::now() }; + // Wall-clock instant. + std::chrono::system_clock::time_point wallTime{ std::chrono::system_clock::now() }; + + MetaBag metadata; + + // Causal error, allows wrapping lower-level failures with higher-level context. + std::shared_ptr cause; + +public: + /// @brief Adds metadata to the error record. + /// @param[in] key The metadata key (e.g. "filename", "userId", etc.) + /// @param[in] value The metadata value (arbitrary string, e.g. "foo.txt", "12345", etc.) + void addMetadata(gp::String key, gp::String value); + + /// @brief Looks up a metadata value by key. + /// @param[in] key The metadata key to look up. + /// @return The metadata value associated with the key, or empty string view if not found. + GP_NODISCARD gp::StringView getMetadata(gp::StringView key) const noexcept; + + /// @brief Checks if the error has a causal error. + /// @return True if the error has a causal error, false otherwise. + GP_NODISCARD bool hasCause() const noexcept; + + /// @brief Gets the depth of the causal error chain. + /// @return The number of levels in the causal error chain. + GP_NODISCARD gp::USize causeDepth() const noexcept; + + /// @brief Generates a summary of the error. + /// @return The error summary as a string. + GP_NODISCARD gp::String summary() const; + + /// @brief Generates a full report of the error. + /// @return The full error report as a string. + GP_NODISCARD gp::String fullReport() const; +}; + +}; // namespace gp::error diff --git a/source/runtime/core/public/errors/ErrorRegistry.hpp b/source/runtime/core/public/errors/ErrorRegistry.hpp new file mode 100644 index 0000000..644d648 --- /dev/null +++ b/source/runtime/core/public/errors/ErrorRegistry.hpp @@ -0,0 +1,253 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "container/BasicString.hpp" // IWYU pragma: keep +#include "container/Forward.hpp" +#include "container/Optional.hpp" +#include "CoreMinimal.hpp" +#include "errors/ErrorCode.hpp" +#include // TODO: gp::Mutex? +#include // TODO: gp::HashMap? + +namespace gp::error +{ + +/// @brief Metadata registry for error codes. +struct ErrorEntry +{ + gp::String description; // m_table; + +public: + /// @brief Delete copy/move constructors and assignment operators to enforce singleton pattern. + ErrorRegistry(const ErrorRegistry&) = delete; + ErrorRegistry& operator=(const ErrorRegistry&) = delete; + +private: + /// @brief Private constructor to prevent external instantiation, registers all built-in codes. + ErrorRegistry(); + +public: + /// @brief Registers a new ErrorCode with its associated metadata in the registry. + /// @param[in] code The ErrorCode to register. + /// @param[in] entry The ErrorEntry containing the metadata for the code. + void registerCode(ErrorCode code, ErrorEntry entry); + + /// @brief Looks up the given ErrorCode in the registry and returns its associated ErrorEntry if found. + /// @param[in] code The ErrorCode to look up in the registry. + /// @return An optional ErrorEntry containing the metadata if the code is registered; otherwise, an empty optional. + GP_NODISCARD gp::Optional lookup(ErrorCode code) const; + + /// @brief Gets the human-readable description associated with the given ErrorCode. + /// @note + /// Returns a copy of the description string, safe to hold across concurrent registry mutations. + /// @param[in] code The ErrorCode for which to retrieve the description. + /// @return A String containing the description if the code is registered; otherwise, "". + GP_NODISCARD gp::String describe(ErrorCode code) const; + + /// @brief Gets the remediation hint associated with the given ErrorCode, if available. + /// @param[in] code The ErrorCode for which to retrieve the remediation hint. + /// @return A String containing the remediation hint if it exists; otherwise, an empty string. + GP_NODISCARD gp::String remediationHint(ErrorCode code) const; + + /// @brief Gets the documentation URL associated with the given ErrorCode, if available. + /// @param[in] code The ErrorCode for which to retrieve the documentation URL. + /// @return A String containing the documentation URL if it exists; otherwise, an empty string. + GP_NODISCARD gp::String wikiUrl(ErrorCode code) const; + + /// @brief Checks if the given ErrorCode is registered and marked as expected (i.e. not a bug). + /// @param[in] code The ErrorCode to check for expected status. + /// @return True if the code is registered and marked as expected; otherwise, false. + GP_NODISCARD bool isExpected(ErrorCode code) const; + + /// @brief Checks if the given ErrorCode is registered and marked as always fatal. + /// @param[in] code The ErrorCode to check for fatality. + /// @return True if the code is registered and marked as always fatal; otherwise, false. + GP_NODISCARD bool isAlwaysFatal(ErrorCode code) const; + + /// @brief Returns the total number of registered error codes in the registry. + /// @return The count of registered error codes. + GP_NODISCARD gp::USize size() const; + + /// @brief Dumps the entire registry as a human-readable string (for tooling/debug). + /// @return A formatted string containing all registered error codes and their metadata. + GP_NODISCARD gp::String dumpAll() const; + +private: + /// @brief Registers all built-in error codes with their descriptions and remediation hints. + void registerBuiltins(); + +public: + /// @brief Singleton accessor for the ErrorRegistry instance. + /// @return Reference to the singleton ErrorRegistry instance. + static ErrorRegistry& instance() noexcept + { + static ErrorRegistry s_instance; + return s_instance; + } +}; + +// Initialize the registry with built-in codes and their metadata. +inline void ErrorRegistry::registerBuiltins() +{ + auto reg = [&](ErrorCode code, + gp::String description, + gp::String remediation, + gp::String wikiUrl = {}, + bool expected = false, + bool alwaysFatal = false) + { + registerCode( + code, + ErrorEntry{ .description = std::move(description), + .remediation = std::move(remediation), + .wikiUrl = std::move(wikiUrl), + .isExpected = expected, + .isAlwaysFatal = alwaysFatal } + ); + }; + + using namespace codes; + + // clang-format off + + // Generic / Uncategorized + reg(kUnknown, "Unknown error", "Consult the full stack trace."); + reg(kNotImplemented, "Feature not implemented", "This code path is a stub. Implement or route around it."); + reg(kInvalidArgument, "Invalid argument", "Check the calling convention and argument constraints."); + reg(kOutOfRange, "Value out of range", "Clamp or validate inputs before passing to this API."); + reg(kNullPointer, "Null pointer dereference", "Ensure the object was successfully constructed before use.", {}, false, true); + reg(kTimeout, "Operation timed out", "Increase the timeout budget or check for deadlocks."); + reg(kNotFound, "Resource not found", "Verify the identifier and that the resource is registered.", {}, true); + reg(kAlreadyExists, "Resource already exists", "Use a unique identifier or destroy the existing resource first."); + reg(kPermission, "Permission denied", "Check OS permissions and file/socket ownership."); + + // Memory + reg(kOutOfMemory, "Out of memory", + "Reduce allocation pressure: pool allocators, streaming, or LOD budgets. " + "Profile with GP MemTracker.", + {}, false, true); + reg(kAlignmentFault, "Alignment fault", + "Ensure allocations meet the required alignment for SIMD types. " + "Use gp::AlignedAlloc()."); + reg(kHeapCorruption, "Heap corruption detected", + "Enable GP_HEAP_GUARD in debug builds and inspect surrounding allocations.", + {}, false, true); + + // IO + reg(kFileNotFound, "File not found", "Verify the path and check the virtual filesystem mount table.", {}, true); + reg(kFileOpenFailed, "File open failed", "Check read/write permissions and ensure the path is not locked."); + reg(kFileReadFailed, "File read failed", "The file may be truncated or the handle was closed prematurely."); + reg(kFileWriteFailed,"File write failed", "Check disk space and write permissions."); + reg(kEOF, "End of file reached", "Normal termination condition, not a bug.", {}, true); + + // RHI / Renderer + reg(kDeviceLost, "GPU device lost", + "Possible causes: driver crash, TDR timeout, GPU hot-unplug. " + "Attempt device recovery or present a safe-mode fallback.", + {}, false, true); + reg(kSwapchainFail, "Swapchain creation or present failed", + "Verify the window is valid and has a non-zero client area. " + "Handle DXGI_ERROR_INVALID_CALL / VK_ERROR_OUT_OF_DATE_KHR."); + reg(kShaderCompile, "Shader compilation failed", + "Check the shader source for syntax errors. Run with GP_SHADER_VERBOSE=1 " + "to capture the full compiler log."); + reg(kPipelineCreate,"Pipeline state object creation failed", + "Validate all PSO descriptors. Ensure shaders are compiled and " + "root signatures match."); + + // Platform / Window + reg(kWindowCreate, "Window creation failed", + "Ensure the display server is running, the resolution is supported, " + "and the platform backend is initialized."); + reg(kWindowResize, "Window resize failed", + "The OS may have rejected the requested dimensions. Clamp to monitor bounds."); + reg(kInputInit, "Input system initialization failed", + "Check device permissions (e.g. /dev/input on Linux) and platform backend."); + reg(kNetConnect, "Network connection failed", + "Verify the endpoint address, firewall rules, and that the remote " + "service is reachable."); + + // Audio + reg(kAudioDeviceInit, "Audio device initialization failed", + "Ensure an audio output device is present and not exclusively locked by " + "another process. Check WASAPI / ALSA / CoreAudio permissions."); + + // clang-format on +} + +} // namespace gp::error + +/// @brief Macro to register a custom ErrorCode in the global registry with its description, remediation hint, and +/// optional documentation URL. +/// @param[in] code_ The ErrorCode to register. +/// @param[in] desc_ A short description of what the error code means. +/// @param[in] hint_ An actionable fix or investigation hint for the error code. +/// @param[in] ... Optional variadic argument for the documentation URL (e.g. "https://wiki.mygame.dev/errors/MY_CODE"). +#define GP_REGISTER_ERROR(code_, desc_, hint_, ...) \ + ::gp::error::ErrorRegistry::instance().registerCode( \ + (code_), \ + ::gp::error::ErrorEntry{ \ + .description = (desc_), \ + .remediation = (hint_), \ + .wikiUrl = [] { \ + constexpr const char* _args[] = { __VA_ARGS__ "" }; \ + return gp::String(_args[0]); }(), \ + .isExpected = false, \ + .isAlwaysFatal = false \ + }) + +/// @brief Registers an expected error code with its description and remediation hint. +/// @param[in] code_ The ErrorCode to register as expected. +/// @param[in] desc_ A short description of what the error code means. +/// @param[in] hint_ An actionable fix or investigation hint for the error code. +#define GP_REGISTER_EXPECTED_ERROR(code_, desc_, hint_) \ + ::gp::error::ErrorRegistry::instance().registerCode( \ + (code_), \ + ::gp::error::ErrorEntry{ \ + .description = (desc_), \ + .remediation = (hint_), \ + .wikiUrl = {}, \ + .isExpected = true, \ + .isAlwaysFatal = false \ + }) diff --git a/source/runtime/core/public/errors/Severity.hpp b/source/runtime/core/public/errors/ErrorSeverity.hpp similarity index 91% rename from source/runtime/core/public/errors/Severity.hpp rename to source/runtime/core/public/errors/ErrorSeverity.hpp index b9c4f7a..25791be 100644 --- a/source/runtime/core/public/errors/Severity.hpp +++ b/source/runtime/core/public/errors/ErrorSeverity.hpp @@ -28,6 +28,15 @@ enum class Severity : gp::UInt8 COUNT = 7 //" }; + Severity m_minSeverity{ Severity::Trace }; + gp::Vector m_domainFilter; + +public: + /// @brief Default destructor. + virtual ~Sink() noexcept = default; + +public: + /// @brief Called by the ErrorSystem for every record that passes the global filter. + /// @param[in] record The error record to process. + virtual void onRecord(const ErrorRecord& record) = 0; + + /// @brief Flush any buffered output (file handles, network sockets, etc.). + virtual void flush(); + + /// @brief Get the minimum severity level that this sink will process. + /// @return The minimum severity level. + GP_NODISCARD Severity minSeverity() const noexcept; + + /// @brief Set the minimum severity level that this sink will process. + /// @param[in] severity The minimum severity level to set. + void setMinSeverity(Severity severity) noexcept; + + /// @brief Add a domain filter to this sink. + /// @param[in] domain The domain to filter. + void addDomainFilter(Domain domain); + + /// @brief Clear all domain filters from this sink. + void clearDomainFilter(); + + /// @brief Get the name of this sink. + /// @return The name of the sink. + GP_NODISCARD const gp::String& name() const noexcept; + + /// @brief Set the name of this sink. + /// @param[in] name The name to set. + void setName(gp::String name); + + /// @brief Dispatch an error record to this sink if it meets the severity and domain filters. + /// @param[in] record The error record to dispatch. + void dispatch(const ErrorRecord& record); +}; + +}; // namespace gp::error diff --git a/source/runtime/core/public/errors/ErrorSystem.hpp b/source/runtime/core/public/errors/ErrorSystem.hpp new file mode 100644 index 0000000..b874082 --- /dev/null +++ b/source/runtime/core/public/errors/ErrorSystem.hpp @@ -0,0 +1,203 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "container/Array.hpp" +#include "container/Forward.hpp" +#include "CoreMinimal.hpp" +#include "errors/ErrorConfig.hpp" +#include "errors/ErrorContext.hpp" +#include "errors/ErrorRecord.hpp" +#include "errors/ErrorSink.hpp" +#include "errors/sinks/MultiSink.hpp" +#include "memory/UniquePtr.hpp" +#include // TODO: gp::Atomic? +#include // TODO: gp::Chrono?, gp::TimePoint? +#include // TODO: gp::Mutex? +#include +#include // TODO: gp::Thread, gp::ThreadId? +#include // TODO: gp::UnorderedMap? + +namespace gp::error +{ + +/// @brief Statistics for error counts, tracked per severity level. Queryable at any time via ErrorSystem::stats(). +struct ErrorStatistics +{ +public: + gp::Array, static_cast(Severity::COUNT)> counts{}; + std::atomic totalDropped{ 0u }; //(severity)].load(std::memory_order_relaxed); + } + + /// @brief Increments the count for a given severity level. + /// @param[in] severity The severity level to increment the count for. + void increment(Severity severity) noexcept + { + counts[static_cast(severity)].fetch_add(1, std::memory_order_relaxed); + } +}; + +/// @brief The central error handling system, implemented as a singleton. Provides APIs for raising errors, managing +/// sinks, and querying statistics. +/// @note +/// ErrorSystem is the single-point dispatcher that: +/// 1. Owns the configuration (ErrorSystemConfig). +/// 2. Owns the root MultiSink (fan-out to all registered sinks). +/// 3. Builds ErrorRecords from raise() parameters. +/// 4. Manages deduplication, statistics counters, and crash handlers. +/// 5. Provides structured query/inspection APIs used by crash-report UIs. +class ErrorSystem +{ +private: + ErrorSystemConfig m_config; + gp::UniquePtr m_rootSink; + ErrorStatistics m_stats; + std::chrono::steady_clock::time_point m_initTime; + std::mutex m_dedupMutex; + std::unordered_map m_dedupTable; + + static std::atomic s_instance; + static std::mutex s_initMutex; + static std::mutex m_sinkMutex; + +public: + /// @brief Key used for deduplication of errors. Combines error code, message hash, and thread ID. + struct DedupKey + { + public: + gp::UInt32 codeRaw; // sink); + + /// @brief Removes a sink from the ErrorSystem. + /// @param[in] name The name of the sink to remove. + static void removeSink(const gp::String& name); + + /// @brief Clears all sinks, removing them from the ErrorSystem. + static void clearSinks(); + + /// @brief Flushes all sinks, ensuring any buffered errors are written. + static void flushAll(); + + /// @brief Returns a reference to the root MultiSink for advanced configuration. + GP_NODISCARD static MultiSink& rootSink(); + + /// @brief Returns a const reference to the current ErrorSystemConfig. + /// @return A const reference to the ErrorSystemConfig instance used by the ErrorSystem. + GP_NODISCARD static const ErrorSystemConfig& config() noexcept; + + /// @brief Set the Global Min Severity object + /// @param[in] severity The minimum severity level for errors to be processed by the system. + static void setGlobalMinSeverity(Severity severity) noexcept; + + /// @brief Dispatches an error record to the ErrorSystem, building it from the provided parameters. + /// @param[in] severity The severity level of the error being dispatched. + /// @param[in] code The error code associated with the error being dispatched. + /// @param[in] message The error message string describing the error being dispatched. + /// @param[in] location The source location where the error was raised. + /// @param[in] cause An optional shared pointer to another ErrorRecord that caused this error. + static void dispatch( + Severity severity, + ErrorCode code, + gp::String message, + std::source_location location, + std::shared_ptr cause = nullptr + ); + + /// @brief Returns a reference to the error statistics. + /// @return A const reference to the ErrorStatistics instance containing error counts. + GP_NODISCARD static const ErrorStatistics& stats() noexcept; + + /// @brief Resets all error statistics counts to zero. + static void resetStats() noexcept; + + /// @brief Installs signal handlers for handling system signals that may indicate crashes or other critical events. + static void installSignalHandlers(); + +private: + /// @brief Determines whether an error with the given deduplication key should be emitted or deduplicated based on + /// recent occurrences. + /// @param[in] key The deduplication key representing the error being evaluated for deduplication. + /// @return True if the error should be emitted, false if it should be deduplicated (i.e., suppressed). + GP_NODISCARD bool shouldDedup(const DedupKey& key); + + /// @brief Internal implementation of the dispatch function, which builds an ErrorRecord and sends it to the sinks. + /// This is called by the public static dispatch() method after performing any necessary checks (e.g., + /// deduplication). + /// @param[in] severity The severity level of the error being dispatched. + /// @param[in] code The error code associated with the error being dispatched. + /// @param[in] message The error message string describing the error being dispatched. + /// @param[in] location The source location where the error was raised. + /// @param[in] cause The cause of the error, represented as a shared pointer to another ErrorRecord. This allows for + /// chaining of errors to provide more context. + void dispatchImpl( + Severity severity, + ErrorCode code, + gp::String message, + std::source_location location, + std::shared_ptr cause + ); + + /// @brief Signal handler for handling system signals. + /// @param[in] sig The signal number. + static void onSignal(int sig); +}; + +} // namespace gp::error diff --git a/source/runtime/core/public/errors/Forward.hpp b/source/runtime/core/public/errors/Forward.hpp index e41102e..b3e236f 100644 --- a/source/runtime/core/public/errors/Forward.hpp +++ b/source/runtime/core/public/errors/Forward.hpp @@ -15,9 +15,40 @@ struct AbortConfig; struct FilterConfig; struct ThreadConfig; struct ErrorSystemConfig; +struct CrashReportConfig; + +// Sinks +class Sink; +class AbortSink; +class BreakpointSink; +class ConsoleSink; +class CallbackSink; +class DeferredSink; +class FileSink; +class MultiSink; +class RateLimitedSink; +class TelemetrySink; +class FilteredSink; +class SplitterSink; +class BroadcastSink; +class RingBufferSink; +class BinarySink; +class CrashReporter; // Forward declarations +struct MetaEntry; class ErrorCode; +class ErrorRecord; +class ErrorContext; +struct ContextFrame; +class ContextScope; +struct ErrorStatistics; +class ErrorSystem; +struct ErrorEntry; +class ErrorRegistry; +class FilterPredicate; +class ErrorScope; +class ErrorGuard; } // namespace gp::error diff --git a/source/runtime/core/public/errors/sinks/AbortSink.hpp b/source/runtime/core/public/errors/sinks/AbortSink.hpp new file mode 100644 index 0000000..aca5981 --- /dev/null +++ b/source/runtime/core/public/errors/sinks/AbortSink.hpp @@ -0,0 +1,41 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "errors/ErrorSeverity.hpp" +#include "errors/ErrorSink.hpp" + +namespace gp::error +{ + +/// @brief A sink that terminates the process when an error record with severity at or above a specified threshold is +/// received. +class AbortSink final : public Sink +{ +public: + enum class Mode + { + Abort, + Terminate + }; + +private: + Severity m_abortAt; + Mode m_mode; + +public: + /// @brief Constructs an AbortSink that terminates the process when a record with severity at or above @p abortAt is + /// received. + /// @param[in] abortAt The severity threshold at which to trigger process termination (inclusive). + /// @param[in] mode The method of termination: std::abort() or std::terminate(). + explicit AbortSink(Severity abortAt = Severity::Fatal, Mode mode = Mode::Abort); + +public: + /// @brief Called by the ErrorSystem for every record that passes the global filter. + /// @param[in] record The error record to process. + void onRecord(const ErrorRecord& record) override; +}; + +} // namespace gp::error diff --git a/source/runtime/core/public/errors/sinks/BreakpointSink.hpp b/source/runtime/core/public/errors/sinks/BreakpointSink.hpp new file mode 100644 index 0000000..d527a5b --- /dev/null +++ b/source/runtime/core/public/errors/sinks/BreakpointSink.hpp @@ -0,0 +1,32 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "errors/ErrorSeverity.hpp" +#include "errors/ErrorSink.hpp" + +namespace gp::error +{ + +/// @brief A sink that triggers a debug break when an error record with severity at or above a specified threshold is +/// received. This is useful for developers who want to break into the debugger immediately when a certain level of +/// error occurs. +class BreakpointSink final : public Sink +{ +private: + Severity m_breakAt; + +public: + /// @brief Constructs a BreakpointSink that triggers a debug break at or above the specified severity level. + /// @param[in] breakAt The severity level at which to trigger a debug break (inclusive). + explicit BreakpointSink(Severity breakAt = Severity::Error); + +public: + /// @brief Called by the ErrorSystem for every record that passes the global filter. + /// @param[in] record The error record to process. + void onRecord(const ErrorRecord& record) override; +}; + +} // namespace gp::error diff --git a/source/runtime/core/public/errors/sinks/ConsoleSink.hpp b/source/runtime/core/public/errors/sinks/ConsoleSink.hpp new file mode 100644 index 0000000..5b95e15 --- /dev/null +++ b/source/runtime/core/public/errors/sinks/ConsoleSink.hpp @@ -0,0 +1,47 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "errors/ErrorSink.hpp" +#include // TODO: gp::IO? +#include // TODO: gp::Mutex? + +namespace gp::error +{ + +/// @brief ConsoleSink, ANSI-coloured stderr (or stdout) output. +class ConsoleSink final : public Sink +{ +private: + std::mutex m_mutex; + bool m_useColor{ true }; + FILE* m_stream{ nullptr }; + bool m_printStacktrace{ true }; + bool m_printCause{ true }; + +public: + /// @brief Constructs a ConsoleSink. + /// @param[in] useAnsiColor If true, ANSI color codes will be used to colorize the output based on severity. If + /// false, no color codes will be used. + /// @param[in] toStdout If true, output will be sent to stdout; otherwise, it will be sent to stderr. + explicit ConsoleSink(bool useAnsiColor = true, bool toStdout = false); + +public: + /// @brief Called by the ErrorSystem for every record that passes the global filter. + /// @param[in] record The error record to process. + void onRecord(const ErrorRecord& record) override; + /// @brief Flush any buffered output (file handles, network sockets, etc.). + void flush() override; + + /// @brief Enable or disable printing of stack traces. + /// @param[in] enabled If true, stack traces will be printed; otherwise, they will be omitted. + void setPrintStacktrace(bool enabled) noexcept; + + /// @brief Enable or disable printing of error causes. + /// @param[in] enabled If true, error causes will be printed; otherwise, they will be omitted. + void setPrintCause(bool enabled) noexcept; +}; + +} // namespace gp::error diff --git a/source/runtime/core/public/errors/sinks/FileSink.hpp b/source/runtime/core/public/errors/sinks/FileSink.hpp new file mode 100644 index 0000000..14e6eb8 --- /dev/null +++ b/source/runtime/core/public/errors/sinks/FileSink.hpp @@ -0,0 +1,35 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "errors/ErrorSink.hpp" +#include // TODO: gp::fs::FileStream? +#include // TODO: gp::Mutex? + +namespace gp::error +{ + +/// @brief A simple sink that appends UTF-8 text to a file. +class FileSink final : public Sink +{ +private: + std::mutex m_mutex; + std::ofstream m_file; + +public: + /// @brief Constructs a FileSink that appends UTF-8 text to the specified path. + /// @param[in] path The file path to write logs to. + explicit FileSink(const gp::String& path); + +public: + /// @brief Called by the ErrorSystem for every record that passes the global filter. + /// @param[in] record The error record to process. + void onRecord(const ErrorRecord& record) override; + + /// @brief Flush any buffered output (file handles, network sockets, etc.). + void flush() override; +}; + +} // namespace gp::error diff --git a/source/runtime/core/public/errors/sinks/MultiSink.hpp b/source/runtime/core/public/errors/sinks/MultiSink.hpp new file mode 100644 index 0000000..4cfae1c --- /dev/null +++ b/source/runtime/core/public/errors/sinks/MultiSink.hpp @@ -0,0 +1,53 @@ +// Copyright (c) - Graphical Playground. All rights reserved. +// For more information, see https://graphical-playground/legal +// mailto:support AT graphical-playground DOT com + +#pragma once + +#include "container/Forward.hpp" +#include "container/Vector.hpp" +#include "CoreMinimal.hpp" +#include "errors/ErrorSink.hpp" +#include // TODO: gp::Mutex? + +namespace gp::error +{ + +/// @brief MultiSink is the default root sink for the ErrorSystem. It fans out to any number of child sinks, which can +/// be added and removed at runtime. +/// @note +/// MultiSink is thread-safe, but individual child sinks are not protected by any internal synchronization. +/// If you need to share a sink across threads, consider wrapping it in a RateLimitedSink or using your own +/// synchronization. +class MultiSink final : public Sink +{ +private: + mutable std::mutex m_mutex; + gp::Vector> m_sinks; + +public: + /// @brief Constructor that initializes the MultiSink with a default name and minimum severity level. + explicit MultiSink(); + +public: + /// @brief Add a sink to this MultiSink. + /// @param[in] sink The sink to add. + void addSink(std::shared_ptr sink); + + /// @brief Remove a sink from this MultiSink by name. + /// @param[in] name The name of the sink to remove. + void removeSink(const gp::String& name); + + /// @brief Get the number of sinks currently added to this MultiSink. + /// @return The number of sinks. + GP_NODISCARD gp::USize sinkCount() const noexcept; + + /// @brief Called by the ErrorSystem for every record that passes the global filter. + /// @param[in] record The error record to process. + void onRecord(const ErrorRecord& record) override; + + /// @brief Flush any buffered output (file handles, network sockets, etc.). + void flush() override; +}; + +} // namespace gp::error diff --git a/source/runtime/core/tests/container/BasicString.tests.cpp b/source/runtime/core/tests/container/BasicString.tests.cpp index 94c266b..fc1ba05 100644 --- a/source/runtime/core/tests/container/BasicString.tests.cpp +++ b/source/runtime/core/tests/container/BasicString.tests.cpp @@ -686,4 +686,86 @@ TEST_CASE("String - repeated pushBack across SSO boundary", "[container][String] } } +TEST_CASE("String - formatting a string via gp::String::format", "[container][String][format]") +{ + SECTION("Standard types (integers, floats, C-strings)") + { + gp::String s = gp::String::format("Hello, {}! The answer is {}.", "world", 42); + REQUIRE(s == "Hello, world! The answer is 42."); + + gp::String f = gp::String::format("Pi is roughly {:.2f}", 3.14159); + REQUIRE(f == "Pi is roughly 3.14"); + } + + SECTION("Formatting with gp::String as an argument") + { + gp::String name = "Alice"; + gp::String s = gp::String::format("User name: {}", name); + REQUIRE(s == "User name: Alice"); + } + + SECTION("Formatting with gp::StringView as an argument") + { + // Assuming gp::StringView is the typedef for your BasicStringView + gp::String original = "Bob"; + gp::BasicStringView> view = original; + + gp::String s = gp::String::format("User view: {}", view); + REQUIRE(s == "User view: Bob"); + } + + SECTION("Multiple gp::String and gp::StringView arguments combined") + { + gp::String first = "Graphical"; + gp::StringView second = "Playground"; + + gp::String s = gp::String::format("{} {}", first, second); + REQUIRE(s == "Graphical Playground"); + } + + SECTION("Format specifiers: Width, Fill, and Alignment") + { + // Because we inherited from std::formatter, + // these should work automatically for gp::String and gp::StringView! + gp::String str = "C++"; + gp::StringView view = "20"; + + // Right align, width 6, padded with spaces + REQUIRE(gp::String::format("{:>6}", str) == " C++"); + + // Left align, width 6, padded with dashes + REQUIRE(gp::String::format("{:-<6}", view) == "20----"); + + // Center align, width 9, padded with asterisks + REQUIRE(gp::String::format("{:*^9}", str) == "***C++***"); + } + + SECTION("Empty strings and views") + { + gp::String emptyStr; + gp::StringView emptyView; + + REQUIRE(gp::String::format("[{}]", emptyStr) == "[]"); + REQUIRE(gp::String::format("[{}]", emptyView) == "[]"); + REQUIRE(gp::String::format("{}", gp::String("")) == ""); + } + + SECTION("Heap allocation (Exceeding SSO buffer)") + { + // gp::BasicString has a 23-character SSO limit. + // We want to format a string that guarantees a heap allocation. + gp::String s1 = "This is a string "; + gp::String s2 = "that is definitely way longer than 23 characters."; + + gp::String result = gp::String::format("{}{}", s1, s2); + + REQUIRE(result.size() > 23); + REQUIRE(result == "This is a string that is definitely way longer than 23 characters."); + + // Verify formatting a long gp::String into another format works + gp::String wrapped = gp::String::format("<<{}>>", result); + REQUIRE(wrapped == "<>"); + } +} + } // namespace gp::tests diff --git a/source/runtime/core/tests/container/Vector.tests.cpp b/source/runtime/core/tests/container/Vector.tests.cpp index f52c3b8..8df79cc 100644 --- a/source/runtime/core/tests/container/Vector.tests.cpp +++ b/source/runtime/core/tests/container/Vector.tests.cpp @@ -671,4 +671,92 @@ TEST_CASE("Vector - custom allocator basic operations", "[container][Vector][all REQUIRE(lin.getAllocatedSize() > 0); } +TEST_CASE("Vector - eraseIf", "[container][Vector]") +{ + gp::Vector v = { 1, 2, 3, 4, 5, 6 }; + + SECTION("Removes elements matching the predicate and returns count") + { + auto removedCount = gp::eraseIf( + v, + [](int x) + { + return x % 2 == 0; + } + ); // Remove evens + + REQUIRE(removedCount == 3); + REQUIRE(v.size() == 3); + REQUIRE(v[0] == 1); + REQUIRE(v[1] == 3); + REQUIRE(v[2] == 5); + } + + SECTION("Returns 0 and modifies nothing if no elements match") + { + auto removedCount = gp::eraseIf( + v, + [](int x) + { + return x > 10; + } + ); + + REQUIRE(removedCount == 0); + REQUIRE(v.size() == 6); + REQUIRE(v[0] == 1); // Spot check + } + + SECTION("Clears the vector if all elements match") + { + auto removedCount = gp::eraseIf( + v, + [](int) + { + return true; + } + ); + + REQUIRE(removedCount == 6); + REQUIRE(v.isEmpty()); + REQUIRE(v.size() == 0); + } +} + +TEST_CASE("Vector - erase", "[container][Vector]") +{ + gp::Vector v = { 1, 2, 3, 2, 4, 2, 5 }; + + SECTION("Removes all exact matches of a value and returns count") + { + auto removedCount = gp::erase(v, 2); + + REQUIRE(removedCount == 3); + REQUIRE(v.size() == 4); + REQUIRE(v[0] == 1); + REQUIRE(v[1] == 3); + REQUIRE(v[2] == 4); + REQUIRE(v[3] == 5); + } + + SECTION("Returns 0 and modifies nothing if value is not found") + { + auto removedCount = gp::erase(v, 99); + + REQUIRE(removedCount == 0); + REQUIRE(v.size() == 7); + } + + SECTION("Works correctly when the target value is at the boundaries") + { + gp::Vector edgeVector = { 9, 1, 2, 9 }; + auto removedCount = gp::erase(edgeVector, 9); + + REQUIRE(removedCount == 2); + REQUIRE(edgeVector.size() == 2); + REQUIRE(edgeVector[0] == 1); + REQUIRE(edgeVector[1] == 2); + } +} + } // namespace gp::tests