diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f35483..a512f11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,37 @@ jobs: - name: Test run: ctest --test-dir build --output-on-failure + bench-asan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - run: git submodule update --init --recursive + - name: Configure benchmarks (asan) + run: | + cmake -S . -B build-bench-asan \ + -DLOGIT_BENCH_ENABLE=ON \ + -DLOGIT_BENCH_WITH_SPDLOG=ON \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_CXX_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g' \ + -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined' + - name: Build benchmarks (asan) + run: cmake --build build-bench-asan --target logit_bench + - name: Run spdlog async null bench (asan) + timeout-minutes: 10 + env: + LOGIT_BENCH_FILTER_LIB: spdlog + LOGIT_BENCH_FILTER_ASYNC: "1" + LOGIT_BENCH_FILTER_SINK: null + LOGIT_BENCH_FILTER_PRODUCERS: "4" + LOGIT_BENCH_FILTER_BYTES: "40" + LOGIT_BENCH_TOTAL: 200 + LOGIT_BENCH_WARMUP: 20 + LOGIT_BENCH_TIMEOUT_SEC: 120 + run: ./build-bench-asan/logit_bench + vcpkg-install: runs-on: ubuntu-latest env: diff --git a/.github/workflows/odr_check.yml b/.github/workflows/odr_check.yml new file mode 100644 index 0000000..41bf517 --- /dev/null +++ b/.github/workflows/odr_check.yml @@ -0,0 +1,41 @@ +name: ODR checks + +on: + push + +jobs: + odr: + runs-on: ubuntu-latest + defaults: + run: + working-directory: tests/odr + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential + + - name: Build Logit ODR header + run: | + g++ -std=c++17 -I ../../include -I ../../libs/time-shield-cpp/include -c get_logger_a.cpp + g++ -std=c++17 -I ../../include -I ../../libs/time-shield-cpp/include -c get_logger_b.cpp + g++ -std=c++17 -I ../../include -I ../../libs/time-shield-cpp/include -c get_logger_main.cpp + g++ get_logger_a.o get_logger_b.o get_logger_main.o -o app_logger + + - name: Run ConsoleApp ODR header + run: ./app_logger + + - name: Build Task Executor ODR header + run: | + g++ -std=c++17 -I ../../include -I ../../libs/time-shield-cpp/include -c get_executor_a.cpp + g++ -std=c++17 -I ../../include -I ../../libs/time-shield-cpp/include -c get_executor_b.cpp + g++ -std=c++17 -I ../../include -I ../../libs/time-shield-cpp/include -c get_executor_main.cpp + g++ get_executor_a.o get_executor_b.o get_executor_main.o -o app_executor + + - name: Run Task Executor ODR header + run: ./app_executor + \ No newline at end of file diff --git a/bench/LatencyRecorder.hpp b/bench/LatencyRecorder.hpp index f25a84e..a9b65b5 100644 --- a/bench/LatencyRecorder.hpp +++ b/bench/LatencyRecorder.hpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -36,7 +38,8 @@ class LatencyRecorder { explicit LatencyRecorder(std::size_t total) : m_values(total), m_expected(total), - m_next_slot(0) {} + m_next_slot(0), + m_completed(0) {} /** * Reserve a slot (if record==true) and capture t0 using steady_clock. @@ -61,14 +64,24 @@ class LatencyRecorder { if (!token.active) return; const auto t1_ns = now(); m_values[token.slot] = t1_ns - token.t0_ns; // distinct slots -> no data race + const auto done = m_completed.fetch_add(1, std::memory_order_acq_rel) + 1; + if (done == m_expected) { + std::lock_guard lk(m_wait_mx); + m_wait_cv.notify_all(); + } } std::size_t recorded() const { return m_next_slot.load(std::memory_order_relaxed); } + void wait_for_all() const { + std::unique_lock lk(m_wait_mx); + m_wait_cv.wait(lk, [&]{ return m_completed.load(std::memory_order_acquire) >= m_expected; }); + } + Summary finalize() const { - if (recorded() != m_expected) { + if (recorded() != m_expected || m_completed.load(std::memory_order_acquire) != m_expected) { throw std::runtime_error("Incomplete latency capture"); } std::vector sorted = m_values; @@ -102,6 +115,9 @@ class LatencyRecorder { std::vector m_values; // preallocated; no reallocation const std::size_t m_expected; // total messages to record std::atomic m_next_slot; + std::atomic m_completed; + mutable std::condition_variable m_wait_cv; + mutable std::mutex m_wait_mx; }; } // namespace logit_bench diff --git a/bench/adapters/ILoggerAdapter.hpp b/bench/adapters/ILoggerAdapter.hpp index 508a4ad..da469c0 100644 --- a/bench/adapters/ILoggerAdapter.hpp +++ b/bench/adapters/ILoggerAdapter.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include "../LatencyRecorder.hpp" @@ -18,6 +19,8 @@ class ILoggerAdapter { virtual void log(const LatencyRecorder::Token& token, std::string_view message) = 0; virtual void flush() = 0; + + virtual void set_recorder_handle(std::shared_ptr) {} }; } // namespace logit_bench diff --git a/bench/adapters/SpdlogAdapter.cpp b/bench/adapters/SpdlogAdapter.cpp index 5cb22c1..e1ab0f3 100644 --- a/bench/adapters/SpdlogAdapter.cpp +++ b/bench/adapters/SpdlogAdapter.cpp @@ -3,11 +3,15 @@ #ifdef LOGIT_BENCH_HAVE_SPDLOG #include +#include +#include #include #include +#include #include #include #include +#include #include #include @@ -32,6 +36,12 @@ class SpdlogAdapter::MeasuringSink : public spdlog::sinks::sink { void configure(const Scenario& scenario, LatencyRecorder& recorder) { m_sink = scenario.sink; m_recorder = &recorder; + { + std::lock_guard lock(m_pending_mx); + m_pending.clear(); + m_retired.clear(); + m_pending_count = 0; + } if (m_sink == SinkKind::File) { std::filesystem::create_directories("bench/results"); std::lock_guard lock(m_mutex); @@ -43,14 +53,21 @@ class SpdlogAdapter::MeasuringSink : public spdlog::sinks::sink { } } + void track_token(const LatencyRecorder::Token& token, std::unique_ptr payload) { + std::lock_guard lock(m_pending_mx); + m_pending.push_back(Pending{std::move(payload), token}); + ++m_pending_count; + } + void log(const spdlog::details::log_msg& msg) override { - const auto* payload_ptr = reinterpret_cast(msg.source.funcname); - if (!payload_ptr) { - return; + const char* func = msg.source.funcname; + if (msg.payload.size() == 0 || !func || *func == '\0') { + return; // Flush/control messages have no payload attached. } + + const auto* payload_ptr = reinterpret_cast(func); auto* payload = const_cast(payload_ptr); - consume(*payload); - delete payload; + consume(*payload, payload); } void set_pattern(const std::string&) override {} @@ -64,15 +81,59 @@ class SpdlogAdapter::MeasuringSink : public spdlog::sinks::sink { } } + void complete_pending() { + std::vector pending; + { + std::unique_lock lock(m_pending_mx); + m_pending_cv.wait_for(lock, std::chrono::milliseconds(100), [&]{ return m_pending_count == 0; }); + pending.swap(m_pending); + m_pending_count = 0; + } + + std::vector> retired; + retired.reserve(pending.size()); + for (auto& entry : pending) { + if (entry.token.active && m_recorder) { + m_recorder->complete(entry.token); + } + if (entry.payload) { + retired.push_back(std::move(entry.payload)); + } + } + m_retired.insert(m_retired.end(), + std::make_move_iterator(retired.begin()), + std::make_move_iterator(retired.end())); + } + private: - void consume(const MessagePayload& payload) { - if (payload.token.active && m_recorder) { - m_recorder->complete(payload.token); + void consume(const MessagePayload& payload, MessagePayload* payload_ptr) { + LatencyRecorder::Token token = payload.token; + std::unique_ptr owned; + { + std::lock_guard lock(m_pending_mx); + auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const Pending& p){ return p.payload.get() == payload_ptr; }); + if (it != m_pending.end()) { + token = it->token; + owned = std::move(it->payload); + m_pending.erase(it); + --m_pending_count; + if (m_pending_count == 0) m_pending_cv.notify_all(); + } + } + + const MessagePayload& msg = owned ? *owned : payload; + + if (token.active && m_recorder) { + m_recorder->complete(token); + } + if (owned) { + std::lock_guard lock(m_pending_mx); + m_retired.push_back(std::move(owned)); } if (m_sink == SinkKind::File) { std::lock_guard lock(m_mutex); if (m_file.is_open()) { - m_file << payload.text << '\n'; + m_file << msg.text << '\n'; } } } @@ -81,6 +142,15 @@ class SpdlogAdapter::MeasuringSink : public spdlog::sinks::sink { LatencyRecorder* m_recorder = nullptr; std::ofstream m_file; std::mutex m_mutex; + struct Pending { + std::unique_ptr payload; + LatencyRecorder::Token token; + }; + std::vector m_pending; + std::vector> m_retired; + std::size_t m_pending_count = 0; + std::condition_variable m_pending_cv; + std::mutex m_pending_mx; }; SpdlogAdapter::SpdlogAdapter() = default; @@ -90,6 +160,10 @@ SpdlogAdapter::~SpdlogAdapter() { spdlog::shutdown(); } +void SpdlogAdapter::set_recorder_handle(std::shared_ptr recorder) { + m_recorder_handle = std::move(recorder); +} + void SpdlogAdapter::prepare(const Scenario& scenario, LatencyRecorder& recorder) { m_logger.reset(); m_sink.reset(); @@ -123,11 +197,15 @@ void SpdlogAdapter::log(const LatencyRecorder::Token& token, std::string_view me if (!m_logger) { return; } - auto* payload = new MessagePayload(); + auto payload = std::make_unique(); payload->token = token; payload->text.assign(message.data(), message.size()); - spdlog::source_loc loc{nullptr, 0, reinterpret_cast(payload)}; - m_logger->log(loc, spdlog::level::info, spdlog::string_view_t(payload->text)); + MessagePayload* payload_ptr = payload.get(); + if (m_sink) { + m_sink->track_token(token, std::move(payload)); + } + spdlog::source_loc loc{nullptr, 0, reinterpret_cast(payload_ptr)}; + m_logger->log(loc, spdlog::level::info, spdlog::string_view_t(payload_ptr->text)); } void SpdlogAdapter::flush() { @@ -135,6 +213,7 @@ void SpdlogAdapter::flush() { m_logger->flush(); } if (m_sink) { + m_sink->complete_pending(); m_sink->flush(); } } diff --git a/bench/adapters/SpdlogAdapter.hpp b/bench/adapters/SpdlogAdapter.hpp index b34bf68..d6f75ec 100644 --- a/bench/adapters/SpdlogAdapter.hpp +++ b/bench/adapters/SpdlogAdapter.hpp @@ -24,11 +24,14 @@ class SpdlogAdapter : public ILoggerAdapter { void flush() override; + void set_recorder_handle(std::shared_ptr recorder) override; + private: class MeasuringSink; std::shared_ptr m_logger; std::shared_ptr m_sink; + std::shared_ptr m_recorder_handle; bool m_async = false; }; diff --git a/bench/logit_bench.cpp b/bench/logit_bench.cpp index 98b2974..ae48803 100644 --- a/bench/logit_bench.cpp +++ b/bench/logit_bench.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,50 @@ std::size_t get_env_size_t(const char* name, std::size_t def) { return def; } +struct BenchFilter { + std::optional library; + std::optional async; + std::optional sink; + std::optional producers; + std::optional bytes; + + bool matches(const std::string& lib, + bool async_mode, + SinkKind sink_kind, + std::size_t producer_count, + std::size_t msg_bytes) const { + if (library && *library != lib) return false; + if (async && *async != async_mode) return false; + if (sink && *sink != sink_kind) return false; + if (producers && *producers != producer_count) return false; + if (bytes && *bytes != msg_bytes) return false; + return true; + } +}; + +BenchFilter load_filter() { + BenchFilter filter; + if (const char* v = std::getenv("LOGIT_BENCH_FILTER_LIB")) { + filter.library = std::string(v); + } + if (const char* v = std::getenv("LOGIT_BENCH_FILTER_ASYNC")) { + filter.async = std::string(v) == "1"; + } + if (const char* v = std::getenv("LOGIT_BENCH_FILTER_SINK")) { + std::string s(v); + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); + if (s == "null") filter.sink = SinkKind::Null; + if (s == "file") filter.sink = SinkKind::File; + } + if (const char* v = std::getenv("LOGIT_BENCH_FILTER_PRODUCERS")) { + filter.producers = get_env_size_t("LOGIT_BENCH_FILTER_PRODUCERS", 0); + } + if (const char* v = std::getenv("LOGIT_BENCH_FILTER_BYTES")) { + filter.bytes = get_env_size_t("LOGIT_BENCH_FILTER_BYTES", 0); + } + return filter; +} + std::uint64_t steady_now_ns() { const auto now_tp = std::chrono::steady_clock::now().time_since_epoch(); return std::chrono::duration_cast(now_tp).count(); @@ -119,6 +164,7 @@ std::chrono::nanoseconds run_workload( // Barrier to start together. std::mutex start_mx; std::condition_variable start_cv; + std::condition_variable ready_cv; bool start_flag = false; std::size_t ready = 0; @@ -132,7 +178,7 @@ std::chrono::nanoseconds run_workload( { std::unique_lock lk(start_mx); ++ready; - if (ready == scenario.producers) start_cv.notify_one(); + if (ready == scenario.producers) ready_cv.notify_one(); start_cv.wait(lk, [&]{ return start_flag; }); } for (std::size_t n = 0; n < per_thread[i]; ++n) { @@ -150,7 +196,7 @@ std::chrono::nanoseconds run_workload( std::chrono::steady_clock::time_point t0; { std::unique_lock lk(start_mx); - start_cv.wait(lk, [&]{ return ready == scenario.producers; }); + ready_cv.wait(lk, [&]{ return ready == scenario.producers; }); if (measure_duration) t0 = std::chrono::steady_clock::now(); start_flag = true; start_cv.notify_all(); @@ -176,10 +222,12 @@ ScenarioResult execute_scenario( const Scenario& scenario, std::size_t warmup_messages) { - LatencyRecorder recorder(scenario.total_messages); + auto recorder = std::make_shared(scenario.total_messages); + + adapter.set_recorder_handle(recorder); // Adapter should keep a pointer/ref to recorder and call complete(token) from its sink. - adapter.prepare(scenario, recorder); + adapter.prepare(scenario, *recorder); // Warm-up (no recording, no duration). { @@ -192,7 +240,7 @@ ScenarioResult execute_scenario( << " total=" << warmup_messages; log_info(oss.str()); } - run_workload(adapter, recorder, scenario, warmup_messages, false, false); + run_workload(adapter, *recorder, scenario, warmup_messages, false, false); { std::ostringstream oss; oss << "Warm-up completed lib=" << adapter.library_name() @@ -214,7 +262,7 @@ ScenarioResult execute_scenario( << " total=" << scenario.total_messages; log_info(oss.str()); } - const auto dur = run_workload(adapter, recorder, scenario, scenario.total_messages, true, true); + const auto dur = run_workload(adapter, *recorder, scenario, scenario.total_messages, true, true); { std::ostringstream oss; oss << "Measure completed lib=" << adapter.library_name() @@ -225,13 +273,21 @@ ScenarioResult execute_scenario( log_info(oss.str()); } - const auto sum = recorder.finalize(); + // Ensure async pipelines (e.g., spdlog thread pool) are fully drained before + // destroying the recorder referenced by sinks. + adapter.flush(); + + recorder->wait_for_all(); + const auto sum = recorder->finalize(); double thr = 0.0; if (dur.count() > 0) { const double sec = static_cast(dur.count()) / 1'000'000'000.0; thr = static_cast(scenario.total_messages) / sec; } + + adapter.set_recorder_handle(nullptr); + return ScenarioResult{sum, thr, dur}; } @@ -313,6 +369,8 @@ int main() { const std::size_t warmup_messages = get_env_size_t("LOGIT_BENCH_WARMUP", 4096); const std::size_t timeout_seconds = get_env_size_t("LOGIT_BENCH_TIMEOUT_SEC", 1200); + const BenchFilter filter = load_filter(); + LOGIT_SET_MAX_QUEUE(total_messages); if (timeout_seconds > 0) { @@ -338,6 +396,9 @@ int main() { for (auto sink : sinks) { for (std::size_t producers : producer_counts) { for (std::size_t msg_bytes : message_sizes) { + if (!filter.matches(adapter->library_name(), async_mode, sink, producers, msg_bytes)) { + continue; + } Scenario scenario; scenario.async = async_mode; scenario.sink = sink; diff --git a/docs/TaskExecutor.md b/docs/TaskExecutor.md index f93392f..154c3bb 100644 --- a/docs/TaskExecutor.md +++ b/docs/TaskExecutor.md @@ -191,7 +191,13 @@ const auto lost = LOGIT_GET_DROPPED_TASKS(); `set_queue_policy()`. * The hot-resize barrier uses `m_resizing` and `m_resize_cv` so producers never touch a ring buffer that is being rebuilt. This eliminates the data races that - TSAN previously reported on `try_pop()` vs. buffer assignment. + TSAN previously reported on `try_pop()` vs. buffer assignment. The barrier + only drops once the worker thread fully stops and the queue drains; if a sink + blocks the worker or `QueuePolicy::Block` keeps `m_active_tasks` above the + limit for more than one second, `set_max_queue_size()` abandons the hot + resize, clears `m_resizing`, and leaves the existing ring untouched so + producers cannot wait indefinitely. Non-MPSC builds perform the resize as an + atomic update of `m_max_queue_size`, so they are not subject to this stall. * Non-MPSC builds rely solely on mutexes and had no known data races. * The Emscripten path is single-threaded and should not be used concurrently. diff --git a/include/logit_cpp/logit/detail/MpscRingAny.hpp b/include/logit_cpp/logit/detail/MpscRingAny.hpp index a4c206a..1a747d7 100644 --- a/include/logit_cpp/logit/detail/MpscRingAny.hpp +++ b/include/logit_cpp/logit/detail/MpscRingAny.hpp @@ -112,32 +112,35 @@ namespace logit { namespace detail { /// \brief Try to dequeue value into out. Non-blocking. /// \return true on success; false if queue is empty. bool try_pop(T& out) noexcept { - std::size_t pos = m_dequeue_pos.load(std::memory_order_relaxed); - Cell& c = m_cells[pos % m_cap]; - std::size_t seq = c.m_seq.load(std::memory_order_acquire); - - // When ready, seq == pos + 1 - std::intptr_t diff = - static_cast(seq) - static_cast(pos + 1); - - if (diff == 0) { - if (!m_dequeue_pos.compare_exchange_strong( - pos, pos + 1, - std::memory_order_relaxed, - std::memory_order_relaxed)) { - return false; // Single consumer: should be rare. + for (;;) { + std::size_t pos = m_dequeue_pos.load(std::memory_order_relaxed); + Cell& c = m_cells[pos % m_cap]; + std::size_t seq = c.m_seq.load(std::memory_order_acquire); + + // When ready, seq == pos + 1 + std::intptr_t diff = + static_cast(seq) - static_cast(pos + 1); + + if (diff == 0) { + if (!m_dequeue_pos.compare_exchange_weak( + pos, pos + 1, + std::memory_order_relaxed, + std::memory_order_relaxed)) { + // Spurious failure: retry until we own the slot. + continue; + } + + T* p = reinterpret_cast(&c.m_storage); + out = std::move(*p); + p->~T(); + + // Mark cell free for next cycle. + c.m_seq.store(pos + m_cap, std::memory_order_release); + return true; } - - T* p = reinterpret_cast(&c.m_storage); - out = std::move(*p); - p->~T(); - - // Mark cell free for next cycle. - c.m_seq.store(pos + m_cap, std::memory_order_release); - return true; + + return false; // Empty or not yet published. } - - return false; // Empty or not yet published. } /// \brief Lightweight emptiness check for current consumer position. diff --git a/include/logit_cpp/logit/detail/TaskExecutor.hpp b/include/logit_cpp/logit/detail/TaskExecutor.hpp index 25e42ba..da813c0 100644 --- a/include/logit_cpp/logit/detail/TaskExecutor.hpp +++ b/include/logit_cpp/logit/detail/TaskExecutor.hpp @@ -8,17 +8,16 @@ #include #include -#include "logit/config.hpp" #if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__) #include #include #include -#else - #include - #include - #include - #include - #include + #else + #include + #include + #include + #include + #include #endif // Enable lock-free MPSC ring integration (non-Emscripten) by defining: @@ -324,8 +323,18 @@ namespace logit { namespace detail { // Tell producers to pause before any wait()/stop conditions run. m_resizing.store(true, std::memory_order_release); - // Drain the queue completely. - wait(); + // Drain the queue completely, but do not wait forever if the worker + // is stalled (e.g., blocked sink or backpressure keeping + // m_active_tasks > 0). If we fail to drain before the deadline, + // abort the resize and re-open the barrier so producers can + // continue. + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::seconds(1); + if (!wait_until_idle_(deadline)) { + m_resizing.store(false, std::memory_order_release); + m_resize_cv.notify_all(); + return; + } // Stop the worker so it cannot touch m_mpsc_queue during the resize. std::unique_lock lk(m_queue_mutex); @@ -475,6 +484,15 @@ namespace logit { namespace detail { bool queue_empty_() const noexcept { return m_mpsc_queue.empty(); } + + bool wait_until_idle_(std::chrono::steady_clock::time_point deadline) { + std::unique_lock lock(m_queue_mutex); + return m_queue_condition.wait_until(lock, deadline, [this]() { + return ((queue_empty_() && + m_active_tasks.load(std::memory_order_relaxed) == 0) || + m_stop_flag.load(std::memory_order_acquire)); + }); + } #endif TaskExecutor() diff --git a/include/logit_cpp/logit/utils/argument_utils.hpp b/include/logit_cpp/logit/utils/argument_utils.hpp index b86a4d7..c453641 100644 --- a/include/logit_cpp/logit/utils/argument_utils.hpp +++ b/include/logit_cpp/logit/utils/argument_utils.hpp @@ -42,7 +42,7 @@ namespace logit { /// \param left_it Iterator pointing to the '>' character. /// \param right_it Iterator pointing to the end of the string. /// \return true if the '>' character closes a template argument list, false otherwise. - bool is_closing_template(crev_it_t left_it, crev_it_t right_it) { + inline bool is_closing_template(crev_it_t left_it, crev_it_t right_it) { if (*left_it != '>' || left_it == right_it) return false; --left_it; // move to right while (left_it != right_it && ( diff --git a/include/logit_cpp/logit/utils/path_utils.hpp b/include/logit_cpp/logit/utils/path_utils.hpp index a7299d3..bfc3f23 100644 --- a/include/logit_cpp/logit/utils/path_utils.hpp +++ b/include/logit_cpp/logit/utils/path_utils.hpp @@ -68,7 +68,7 @@ namespace logit { /// \brief Retrieves the directory of the executable file. /// \return A string containing the directory path of the executable. - std::string get_exec_dir() { + inline std::string get_exec_dir() { # ifdef _WIN32 std::vector buffer(MAX_PATH); HMODULE hModule = GetModuleHandle(NULL); @@ -137,7 +137,7 @@ namespace logit { /// \brief Recursively retrieves a list of all files in a directory. /// \param path The directory path to search (UTF-8 encoded). /// \return A vector of strings (UTF-8) containing the full paths of all files found. - std::vector get_list_files(const std::string& path) { + inline std::vector get_list_files(const std::string& path) { std::vector list_files; # ifdef _WIN32 // Use wide versions of functions to correctly handle non-ASCII characters. @@ -222,7 +222,7 @@ namespace logit { /// \brief Extracts the file name from a full file path. /// \param file_path The full file path as a string. /// \return The extracted file name, or the full string if no directory separator is found. - std::string get_file_name(const std::string& file_path) { + inline std::string get_file_name(const std::string& file_path) { # if __cplusplus >= 201703L return fs::u8path(file_path).filename().u8string(); # else @@ -255,7 +255,7 @@ namespace logit { /// \brief Creates directories recursively for the given path using C++17 std::filesystem. /// \param path The directory path to create. /// \throws std::runtime_error if the directories cannot be created. - void create_directories(const std::string& path) { + inline void create_directories(const std::string& path) { # ifdef _WIN32 // Convert UTF-8 string to wide string for Windows std::wstring wide_path = utf8_to_wstring(path); @@ -283,7 +283,7 @@ namespace logit { /// \brief Splits a path into its root and components. /// \param path The path to split. /// \return A PathComponents object containing the root and components of the path. - PathComponents split_path(const std::string& path) { + inline PathComponents split_path(const std::string& path) { PathComponents result; size_t i = 0; size_t n = path.size(); @@ -327,7 +327,7 @@ namespace logit { /// \param file_path The target file path. /// \param base_path The base path from which to compute the relative path. /// \return A string representing the relative path from base_path to file_path. - std::string make_relative(const std::string& file_path, const std::string& base_path) { + inline std::string make_relative(const std::string& file_path, const std::string& base_path) { if (base_path.empty()) return file_path; PathComponents file_pc = split_path(file_path); PathComponents base_pc = split_path(base_path); @@ -390,7 +390,7 @@ namespace logit { /// \brief Creates directories recursively for the given path. /// \param path The directory path to create. /// \throws std::runtime_error if the directories cannot be created. - void create_directories(const std::string& path) { + inline void create_directories(const std::string& path) { if (path.empty()) return; PathComponents path_pc = split_path(path); auto &components = path_pc.components; diff --git a/tests/file_logger_test.cpp b/tests/file_logger_test.cpp index 3480dd5..b84e817 100644 --- a/tests/file_logger_test.cpp +++ b/tests/file_logger_test.cpp @@ -1,9 +1,21 @@ -#define LOGIT_FILE_LOGGER_PATH "." +#define LOGIT_FILE_LOGGER_PATH "file_logger_test_logs" #include #include #include +#if defined(_WIN32) +# include +#else +# include +#endif + int main() { +#if defined(_WIN32) + _mkdir(LOGIT_FILE_LOGGER_PATH); +#else + mkdir(LOGIT_FILE_LOGGER_PATH, 0777); +#endif + LOGIT_ADD_FILE_LOGGER_DEFAULT(); const std::string message = "test log message"; LOGIT_INFO(message); diff --git a/tests/odr/get_executor_a.cpp b/tests/odr/get_executor_a.cpp new file mode 100644 index 0000000..80769c3 --- /dev/null +++ b/tests/odr/get_executor_a.cpp @@ -0,0 +1,6 @@ +#include +#include + +extern "C" logit::detail::TaskExecutor* get_executor_a(){ + return std::addressof(logit::detail::TaskExecutor::get_instance()); +} \ No newline at end of file diff --git a/tests/odr/get_executor_b.cpp b/tests/odr/get_executor_b.cpp new file mode 100644 index 0000000..1922535 --- /dev/null +++ b/tests/odr/get_executor_b.cpp @@ -0,0 +1,6 @@ +#include +#include + +extern "C" logit::detail::TaskExecutor* get_executor_b(){ + return std::addressof(logit::detail::TaskExecutor::get_instance()); +} \ No newline at end of file diff --git a/tests/odr/get_executor_main.cpp b/tests/odr/get_executor_main.cpp new file mode 100644 index 0000000..eb788ce --- /dev/null +++ b/tests/odr/get_executor_main.cpp @@ -0,0 +1,22 @@ +#include +#include + +extern "C" logit::detail::TaskExecutor* get_executor_a(); +extern "C" logit::detail::TaskExecutor* get_executor_b(); + +int main() { + auto* executor_a = get_executor_a(); + auto* executor_b = get_executor_b(); + + std::cout << " Task Executor address: " << static_cast(executor_a) + << " Task Executor B address: " << static_cast(executor_b) + << std::endl; + + if (executor_a != executor_b) { + std::cout << "There are 2 different Task Executor instances!" << std::endl; + return 1; + } + + std::cout << "There's only Task Executor, singlton works correctly" << std::endl; + return 0; +} diff --git a/tests/odr/get_logger_a.cpp b/tests/odr/get_logger_a.cpp new file mode 100644 index 0000000..0953389 --- /dev/null +++ b/tests/odr/get_logger_a.cpp @@ -0,0 +1,6 @@ +#include +#include + +extern "C" logit::Logger* get_logger_a(){ + return std::addressof(logit::Logger::get_instance()); +} \ No newline at end of file diff --git a/tests/odr/get_logger_b.cpp b/tests/odr/get_logger_b.cpp new file mode 100644 index 0000000..7ba7661 --- /dev/null +++ b/tests/odr/get_logger_b.cpp @@ -0,0 +1,6 @@ +#include +#include + +extern "C" logit::Logger* get_logger_b(){ + return std::addressof(logit::Logger::get_instance()); +} \ No newline at end of file diff --git a/tests/odr/get_logger_main.cpp b/tests/odr/get_logger_main.cpp new file mode 100644 index 0000000..8dc8721 --- /dev/null +++ b/tests/odr/get_logger_main.cpp @@ -0,0 +1,22 @@ +#include +#include + +extern "C" logit::Logger* get_logger_a(); +extern "C" logit::Logger* get_logger_b(); + +int main() { + auto* logger_a = get_logger_a(); + auto* logger_b = get_logger_b(); + + std::cout << "Logger A address: " << static_cast(logger_a) + << " Logger B address: " << static_cast(logger_b) + << std::endl; + + if (logger_a != logger_b) { + std::cout << "There are 2 different Logger instances!" << std::endl; + return 1; + } + + std::cout << "There's only Logger, singlton works correctly" << std::endl; + return 0; +}