Skip to content
Merged
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
101 changes: 101 additions & 0 deletions test/ScopeTimerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class ScopeTimer_TestFriend {
test_sink_flush_size_is_bounded();
test_async_sink_flushes_on_disable();
test_async_sink_flush_calls_custom_sink();
test_async_sink_worker_can_restart_after_self_shutdown();
test_async_sink_reconfiguration_keeps_worker_running();
test_custom_sink_reconfiguration_is_synchronized();
test_hot_path_timer_emits_compact_line();
Expand Down Expand Up @@ -160,6 +161,7 @@ class ScopeTimer_TestFriend {
test_logfile_null_branch();
test_logfile_failure_cache_branch();
test_default_sink_rejects_symlink_logfile();
test_default_sink_rejects_hard_linked_logfile();
test_static_lifetime_child_probes();
test_log_fd_has_cloexec();

Expand Down Expand Up @@ -1091,6 +1093,17 @@ class ScopeTimer_TestFriend {
SCOPE_TIMER_ENABLE_THREAD_BUFFERED_SINK(1024U);
const auto baseline = ::xyzzy::scopetimer::ScopeTimer::snapshotThreadBuffers().size();

{
auto expiredState = std::make_shared<::xyzzy::scopetimer::ScopeTimer::ThreadBufferState>();
{
std::lock_guard lock(::xyzzy::scopetimer::ScopeTimer::threadBufferRegistryMutex());
::xyzzy::scopetimer::ScopeTimer::threadBufferRegistry().emplace_back(expiredState);
}
}
const auto afterExpiredEntry = ::xyzzy::scopetimer::ScopeTimer::snapshotThreadBuffers().size();
expect(afterExpiredEntry == baseline,
"thread-buffer registry prunes expired weak entries during snapshots");

std::thread worker([] {
::xyzzy::scopetimer::ScopeTimer::threadBufferedSinkWrite("x", 1U);
});
Expand Down Expand Up @@ -1136,6 +1149,12 @@ class ScopeTimer_TestFriend {
asyncState.running = false;
}
expect(queueStayedBounded, "async sink drops records when its queue budget is exhausted");

::xyzzy::scopetimer::ScopeTimer::asyncSinkWrite(
"oversized",
::xyzzy::scopetimer::ScopeTimer::MaxAsyncSinkQueuedBytes + 1U
);
expect(true, "async sink rejects records larger than its queue budget");
}

static void test_async_sink_flushes_on_disable() {
Expand Down Expand Up @@ -1190,6 +1209,40 @@ class ScopeTimer_TestFriend {
::xyzzy::scopetimer::ScopeTimer::setLogSinkForTests(nullptr, nullptr);
}

static void test_async_sink_worker_can_restart_after_self_shutdown() {
std::atomic<std::size_t> writeCount{0U};
::xyzzy::scopetimer::ScopeTimer::setLogSinkForTests(
[&writeCount](const char*, std::size_t) {
writeCount.fetch_add(1U, std::memory_order_relaxed);
::xyzzy::scopetimer::ScopeTimer::asyncSinkFlush();
::xyzzy::scopetimer::ScopeTimer::shutdownAsyncSink();
},
{}
);
SCOPE_TIMER_ENABLE_ASYNC_SINK(1U);
::xyzzy::scopetimer::ScopeTimer::asyncSinkWrite("x", 1U);
::xyzzy::scopetimer::ScopeTimer::asyncSinkFlush();

bool restarted = false;
for (int attempt = 0; attempt < 100 && !restarted; ++attempt) {
{
auto& state = ::xyzzy::scopetimer::ScopeTimer::asyncSinkState();
std::lock_guard lock(state.mutex);
restarted = state.running && state.worker.joinable() && !state.stop;
}
if (!restarted) {
std::this_thread::sleep_for(1ms);
}
}

expect(writeCount.load(std::memory_order_relaxed) == 1U,
"async sink worker invokes the custom sink once before self-shutdown");
expect(restarted,
"async sink worker restarts after a callback requests self-shutdown");
SCOPE_TIMER_DISABLE_ASYNC_SINK();
::xyzzy::scopetimer::ScopeTimer::setLogSinkForTests(nullptr, nullptr);
}

static void test_async_sink_reconfiguration_keeps_worker_running() {
sinkCaptureBuffer().clear();
sinkFlushCount() = 0U;
Expand Down Expand Up @@ -1486,6 +1539,17 @@ class ScopeTimer_TestFriend {
expect(reservedOut == reserved,
"appendBytesTruncatingReserved preserves fully reserved output space");

char* exhaustedOut = reserved + sizeof(reserved);
::xyzzy::scopetimer::ScopeTimer::appendBytesTruncatingReserved(
exhaustedOut,
reserved + sizeof(reserved),
"ignored",
7U,
0U
);
expect(exhaustedOut == reserved + sizeof(reserved),
"appendBytesTruncatingReserved ignores writes after the output buffer is exhausted");

char threadId[16] = {};
char* threadIdOut = threadId;
::xyzzy::scopetimer::ScopeTimer::appendThreadIdTruncatingReserved(
Expand Down Expand Up @@ -2055,6 +2119,43 @@ class ScopeTimer_TestFriend {
::xyzzy::scopetimer::ScopeTimer::resetLogDirectoryForTests(s_test_log_directory);
}

static void test_default_sink_rejects_hard_linked_logfile() {
char templ[] = "/tmp/scopetimer_hardlinkXXXXXX";
char* rawDir = ::mkdtemp(templ);
if (!rawDir) {
expect(false, "created temporary directory for hard-link log test");
return;
}
const std::string dir = rawDir;
const std::string logfile = dir + "/ScopeTimer.log";
const std::string alias = dir + "/ScopeTimer.alias";
{
std::ofstream output(logfile, std::ios::binary);
output << "sentinel";
}
const int linkResult = ::link(logfile.c_str(), alias.c_str());

::setenv("SCOPE_TIMER_DIR", dir.c_str(), 1);
::xyzzy::scopetimer::ScopeTimer::resetLogDirectoryForTests(dir);
::xyzzy::scopetimer::ScopeTimer::closeLogFdForTests();
::xyzzy::scopetimer::ScopeTimer::defaultSinkWrite("must-not-append", 15U);

std::ifstream input(logfile, std::ios::binary);
std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
expect(
linkResult == 0
&& ::xyzzy::scopetimer::ScopeTimer::defaultLogFdForTests() == -1
&& content == "sentinel",
"default sink refuses hard-linked ScopeTimer.log files"
);

::unlink(alias.c_str());
std::remove(logfile.c_str());
::rmdir(dir.c_str());
::setenv("SCOPE_TIMER_DIR", s_test_log_directory.c_str(), 1);
::xyzzy::scopetimer::ScopeTimer::resetLogDirectoryForTests(s_test_log_directory);
}

static void test_static_lifetime_child_probes() {
for (const char* mode : {"static_buffered", "static_async", "static_failure_cache"}) {
const int rc = run_child_with_env({
Expand Down