Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ jobs:
cmake --build "${build_dir}" --parallel
ctest --test-dir "${build_dir}" --verbose

echo "--- Running tests in parallel mode ---"
"${build_dir}/unit_tests" --parallel

- name: Show compiler cache stats
shell: bash
run: |
Expand Down
3 changes: 3 additions & 0 deletions include/eventide/async/io/stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ class tcp : public stream {
int port,
options opts = options(),
event_loop& loop = event_loop::current());

/// Query the local address/port of a listening acceptor.
static result<int> local_port(acceptor& acc);
};

/// TTY/console wrapper.
Expand Down
1 change: 1 addition & 0 deletions include/eventide/zest/detail/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum class TestState {
struct TestAttrs {
bool skip = false;
bool focus = false;
bool serial = false;
};

struct TestCase {
Expand Down
20 changes: 19 additions & 1 deletion include/eventide/zest/detail/suite.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@

namespace eventide::zest {

/// Merge suite-level and case-level test attributes.
/// Case-level flags override suite defaults when explicitly set to true.
constexpr TestAttrs merge_attrs(TestAttrs suite, TestAttrs test_case) {
return {
.skip = suite.skip || test_case.skip,
.focus = suite.focus || test_case.focus,
.serial = suite.serial || test_case.serial,
};
}

template <fixed_string TestName, typename Derived>
struct TestSuiteDef {
using Self = Derived;
Expand All @@ -30,6 +40,14 @@ struct TestSuiteDef {
std::size_t line,
TestAttrs attrs = {}>
inline static bool _register_test_case = [] {
constexpr auto effective_attrs = [] {
if constexpr(requires { Derived::suite_attrs; }) {
return merge_attrs(Derived::suite_attrs, attrs);
} else {
return attrs;
}
}();

auto run_test = +[] -> TestState {
current_test_state() = TestState::Passed;
Derived test;
Expand All @@ -46,7 +64,7 @@ struct TestSuiteDef {
return current_test_state();
};

test_cases().emplace_back(case_name.data(), path.data(), line, attrs, run_test);
test_cases().emplace_back(case_name.data(), path.data(), line, effective_attrs, run_test);
return true;
}();
};
Expand Down
18 changes: 16 additions & 2 deletions include/eventide/zest/macro.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,30 @@
#define TEST_SUITE(name, ...) \
struct name##TEST : __VA_OPT__(__VA_ARGS__, )::eventide::zest::TestSuiteDef<#name, name##TEST>

// clang-format off
#define ZEST_MAKE_ATTRS(...) \
[] constexpr { \
::eventide::zest::TestAttrs _a{}; \
auto& [skip, focus, serial] = _a; \
__VA_ARGS__; \
return _a; \
}()
// clang-format on

#define TEST_SUITE_ATTRS(...) \
constexpr static ::eventide::zest::TestAttrs suite_attrs = ZEST_MAKE_ATTRS(__VA_ARGS__)

#define TEST_CASE(name, ...) \
void _register_##name() { \
constexpr auto file_name = std::source_location::current().file_name(); \
constexpr auto file_len = std::string_view(file_name).size(); \
(void)_register_suites<>; \
constexpr auto _zest_attrs_ = ZEST_MAKE_ATTRS(__VA_OPT__(__VA_ARGS__)); \
(void)_register_test_case<#name, \
&Self::test_##name, \
::eventide::fixed_string<file_len>(file_name), \
std::source_location::current().line() __VA_OPT__(, ) \
__VA_ARGS__>; \
std::source_location::current().line(), \
_zest_attrs_>; \
} \
void test_##name()

Expand Down
4 changes: 4 additions & 0 deletions include/eventide/zest/run.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ struct RunnerOptions {
std::string filter;
/// When true, per-test output is limited to failing cases; the final summary is still printed.
bool only_failed_output = false;
/// When true, test cases are executed in parallel across a thread pool.
bool parallel = false;
/// Number of worker threads for parallel mode (0 = hardware_concurrency).
unsigned parallel_workers = 0;
};

/// Parse CLI arguments into RunnerOptions and execute registered tests.
Expand Down
21 changes: 21 additions & 0 deletions src/async/io/acceptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,25 @@ result<tcp::acceptor>
return tcp::acceptor(std::move(self));
}

result<int> tcp::local_port(tcp::acceptor& acc) {
if(!acc.self) {
return outcome_error(error::invalid_argument);
}

sockaddr_storage storage{};
int namelen = sizeof(storage);
int err = uv_tcp_getsockname(&acc->tcp, reinterpret_cast<sockaddr*>(&storage), &namelen);
if(err != 0) {
return outcome_error(uv::status_to_error(err));
}

if(storage.ss_family == AF_INET) {
return ntohs(reinterpret_cast<sockaddr_in*>(&storage)->sin_port);
} else if(storage.ss_family == AF_INET6) {
return ntohs(reinterpret_cast<sockaddr_in6*>(&storage)->sin6_port);
}

return outcome_error(error::invalid_argument);
}

} // namespace eventide
26 changes: 22 additions & 4 deletions src/async/io/fs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -617,11 +617,29 @@ task<std::vector<fs::dirent>, error> fs::readdir(fs::dir_handle& dir, event_loop

namespace {

/// Returns the default loop handle for synchronous libuv fs calls.
/// libuv sync operations (cb = nullptr) don't actually interact with the
/// event loop, so uv_default_loop() is safe to use from any thread.
/// Returns a thread-local loop handle for synchronous libuv fs calls.
/// libuv sync operations (cb = nullptr) don't actually run the event loop,
/// but uv_default_loop() is a process-global singleton and is NOT thread-safe.
/// Using a thread-local loop avoids data races when sync fs calls are made
/// from multiple threads concurrently.
uv_loop_t* sync_loop() noexcept {
return uv_default_loop();
static thread_local struct sync_loop_holder {
uv_loop_t loop{};

sync_loop_holder() {
if(uv_loop_init(&loop) != 0) {
std::terminate();
}
}

~sync_loop_holder() {
if(uv_loop_close(&loop) != 0) {
std::terminate();
}
}
} holder;

return &holder.loop;
}

} // namespace
Expand Down
12 changes: 9 additions & 3 deletions src/async/libuv.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cstdint>
#include <deque>
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
Expand Down Expand Up @@ -546,10 +547,15 @@ ALWAYS_INLINE std::size_t udp_get_send_queue_count(const uv_udp_t& handle) noexc
return ::uv_udp_get_send_queue_count(&handle);
}

ALWAYS_INLINE error spawn(uv_loop_t& loop,
uv_process_t& process,
const uv_process_options_t& options) noexcept {
/// uv_spawn internally registers a SIGCHLD handler in a process-global
/// red-black tree that is NOT thread-safe. Serialise all spawn calls
/// so that concurrent event-loops on different threads do not race.
inline error spawn(uv_loop_t& loop,
uv_process_t& process,
const uv_process_options_t& options) noexcept {
assert(options.file != nullptr && "uv::spawn requires options.file");
static std::mutex spawn_mutex;
std::lock_guard lock(spawn_mutex);
return status_to_error(::uv_spawn(&loop, &process, &options));
}

Expand Down
Loading
Loading