Files
TheRock/rocm-systems/projects/rocprofiler-systems/source/lib/logger/tests/test_logger.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

657 lines
21 KiB
C++

// Copyright (c) Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT
#include "logger/logger.hpp"
#include "common/env_vars.hpp"
#include <gtest/gtest.h>
#include <sys/wait.h>
#include <unistd.h>
#include <atomic>
#include <cstdlib>
#include <fstream>
#include <string>
#include <thread>
#include <vector>
namespace
{
// Read all available data from a file descriptor into a string.
// Used to collect child stdout redirected through a pipe.
std::string
read_fd(int fd)
{
std::string result;
char buf[4096];
ssize_t n;
while((n = read(fd, buf, sizeof(buf))) > 0)
result.append(buf, static_cast<size_t>(n));
return result;
}
} // namespace
class logger_test : public ::testing::Test
{
protected:
};
TEST_F(logger_test, include_process_id_in_filename_with_extension)
{
auto pid = std::to_string(getpid());
auto result =
rocprofsys::logger_detail::include_process_id_in_filename("logfile.log");
auto expected = "logfile_" + pid + ".log";
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, include_process_id_in_filename_without_extension)
{
auto pid = std::to_string(getpid());
auto result = rocprofsys::logger_detail::include_process_id_in_filename("logfile");
auto expected = "logfile_" + pid;
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, include_process_id_in_filename_with_path)
{
auto pid = std::to_string(getpid());
auto result =
rocprofsys::logger_detail::include_process_id_in_filename("/var/log/myapp.log");
auto expected = "/var/log/myapp_" + pid + ".log";
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, include_process_id_in_filename_with_path_no_extension)
{
auto pid = std::to_string(getpid());
auto result =
rocprofsys::logger_detail::include_process_id_in_filename("/var/log/myapp");
auto expected = "/var/log/myapp_" + pid;
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, include_process_id_in_filename_empty)
{
auto result = rocprofsys::logger_detail::include_process_id_in_filename("");
EXPECT_TRUE(result.empty());
}
TEST_F(logger_test, include_process_id_in_filename_multiple_dots)
{
auto pid = std::to_string(getpid());
auto result = rocprofsys::logger_detail::include_process_id_in_filename(
"file.name.with.dots.txt");
auto expected = "file.name.with.dots_" + pid + ".txt";
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, include_process_id_in_filename_dot_in_directory)
{
auto pid = std::to_string(getpid());
auto result = rocprofsys::logger_detail::include_process_id_in_filename(
"/path.with.dots/logfile.log");
auto expected = "/path.with.dots/logfile_" + pid + ".log";
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, include_process_id_in_filename_hidden_file)
{
auto pid = std::to_string(getpid());
auto result = rocprofsys::logger_detail::include_process_id_in_filename(".hidden");
auto expected = ".hidden_" + pid;
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, include_process_id_in_filename_hidden_file_with_extension)
{
auto pid = std::to_string(getpid());
auto result =
rocprofsys::logger_detail::include_process_id_in_filename(".hidden.log");
auto expected = ".hidden_" + pid + ".log";
EXPECT_EQ(result, expected);
}
TEST_F(logger_test, parse_boolean_env_true_values)
{
EXPECT_TRUE(rocprofsys::parse_boolean_env("1"));
EXPECT_TRUE(rocprofsys::parse_boolean_env("on"));
EXPECT_TRUE(rocprofsys::parse_boolean_env("true"));
EXPECT_TRUE(rocprofsys::parse_boolean_env("yes"));
}
TEST_F(logger_test, parse_boolean_env_false_values)
{
EXPECT_FALSE(rocprofsys::parse_boolean_env("0"));
EXPECT_FALSE(rocprofsys::parse_boolean_env("off"));
EXPECT_FALSE(rocprofsys::parse_boolean_env("false"));
EXPECT_FALSE(rocprofsys::parse_boolean_env("no"));
EXPECT_FALSE(rocprofsys::parse_boolean_env("random"));
EXPECT_FALSE(rocprofsys::parse_boolean_env(nullptr));
}
TEST_F(logger_test, to_lower_conversion)
{
EXPECT_EQ(rocprofsys::to_lower("HELLO"), "hello");
EXPECT_EQ(rocprofsys::to_lower("Hello World"), "hello world");
EXPECT_EQ(rocprofsys::to_lower("already_lower"), "already_lower");
EXPECT_EQ(rocprofsys::to_lower("MiXeD123"), "mixed123");
EXPECT_EQ(rocprofsys::to_lower(""), "");
}
TEST_F(logger_test, logger_settings_parse_level)
{
rocprofsys::logger_settings_t settings;
EXPECT_EQ(settings.parse_level("trace"), spdlog::level::trace);
EXPECT_EQ(settings.parse_level("TRACE"), spdlog::level::trace);
EXPECT_EQ(settings.parse_level("debug"), spdlog::level::debug);
EXPECT_EQ(settings.parse_level("DEBUG"), spdlog::level::debug);
EXPECT_EQ(settings.parse_level("info"), spdlog::level::info);
EXPECT_EQ(settings.parse_level("INFO"), spdlog::level::info);
EXPECT_EQ(settings.parse_level("warn"), spdlog::level::warn);
EXPECT_EQ(settings.parse_level("warning"), spdlog::level::warn);
EXPECT_EQ(settings.parse_level("WARNING"), spdlog::level::warn);
EXPECT_EQ(settings.parse_level("error"), spdlog::level::err);
EXPECT_EQ(settings.parse_level("err"), spdlog::level::err);
EXPECT_EQ(settings.parse_level("ERROR"), spdlog::level::err);
EXPECT_EQ(settings.parse_level("critical"), spdlog::level::critical);
EXPECT_EQ(settings.parse_level("CRITICAL"), spdlog::level::critical);
EXPECT_EQ(settings.parse_level("off"), spdlog::level::off);
EXPECT_EQ(settings.parse_level("OFF"), spdlog::level::off);
EXPECT_EQ(settings.parse_level("invalid"), spdlog::level::info);
}
TEST_F(logger_test, logger_instance_returns_valid_logger)
{
auto& logger = rocprofsys::logger_t::instance();
EXPECT_NE(logger.name(), "");
EXPECT_EQ(logger.name(), "rocprofiler-systems");
testing::internal::CaptureStdout();
logger.info("stdout_capture_test_marker");
logger.flush();
auto captured = testing::internal::GetCapturedStdout();
EXPECT_NE(captured.find("stdout_capture_test_marker"), std::string::npos)
<< "Log message not found in stdout. Captured: " << captured;
EXPECT_NE(captured.find("[info]"), std::string::npos)
<< "Log level not found in stdout. Captured: " << captured;
auto pid_marker = "P:" + std::to_string(getpid());
EXPECT_NE(captured.find(pid_marker), std::string::npos)
<< "PID not found in stdout. Captured: " << captured;
}
TEST_F(logger_test, logger_instance_is_singleton)
{
auto& logger1 = rocprofsys::logger_t::instance();
auto& logger2 = rocprofsys::logger_t::instance();
EXPECT_EQ(&logger1, &logger2);
}
TEST_F(logger_test, fork_child_gets_different_pid_in_filename)
{
pid_t parent_pid = getpid();
pid_t child_pid = fork();
if(child_pid == 0)
{
pid_t current_pid = getpid();
auto child_filename =
rocprofsys::logger_detail::include_process_id_in_filename("test.log");
auto expected_filename = "test_" + std::to_string(current_pid) + ".log";
bool pid_differs = (current_pid != parent_pid);
bool filename_correct = (child_filename == expected_filename);
_exit((pid_differs && filename_correct) ? 0 : 1);
}
else
{
int status;
waitpid(child_pid, &status, 0);
int child_exit_code = WEXITSTATUS(status);
EXPECT_EQ(child_exit_code, 0) << "Child should have different PID in filename";
}
}
TEST_F(logger_test, fork_resets_logger_in_child)
{
auto& parent_logger = rocprofsys::logger_t::instance();
auto* parent_logger_ptr = &parent_logger;
int pipefd[2];
ASSERT_EQ(pipe(pipefd), 0) << "pipe() failed";
pid_t child_pid = fork();
if(child_pid == 0)
{
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
auto& child_logger = rocprofsys::logger_t::instance();
bool logger_has_name = (child_logger.name() == "rocprofiler-systems");
child_logger.info("fork_reset_child_marker");
child_logger.flush();
_exit(logger_has_name ? 0 : 1);
}
close(pipefd[1]);
auto child_output = read_fd(pipefd[0]);
close(pipefd[0]);
int status;
waitpid(child_pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), 0) << "Child logger should be valid after fork reset";
EXPECT_NE(child_output.find("fork_reset_child_marker"), std::string::npos)
<< "Child log message not found in piped stdout. Got: " << child_output;
EXPECT_NE(child_output.find("[info]"), std::string::npos)
<< "Log level not found in child output. Got: " << child_output;
auto child_pid_marker = "P:" + std::to_string(child_pid);
EXPECT_NE(child_output.find(child_pid_marker), std::string::npos)
<< "Child PID not found in log output. Expected P:" << child_pid
<< " in: " << child_output;
auto& post_fork_parent_logger = rocprofsys::logger_t::instance();
EXPECT_EQ(&post_fork_parent_logger, parent_logger_ptr)
<< "Parent logger should remain the same after fork";
}
TEST_F(logger_test, logger_settings_default_values)
{
unsetenv(rocprofsys::env_vars::LOG_LEVEL);
unsetenv(rocprofsys::env_vars::LOG_FILE);
rocprofsys::logger_settings_t settings;
EXPECT_EQ(settings.get_log_level(), spdlog::level::info);
EXPECT_TRUE(settings.get_log_file().empty());
}
TEST_F(logger_test, logger_settings_with_env_vars)
{
setenv(rocprofsys::env_vars::LOG_LEVEL, "debug", 1);
setenv(rocprofsys::env_vars::LOG_FILE, "/tmp/test.log", 1);
rocprofsys::logger_settings_t settings;
EXPECT_EQ(settings.get_log_level(), spdlog::level::debug);
EXPECT_EQ(settings.get_log_file(), "/tmp/test.log");
unsetenv(rocprofsys::env_vars::LOG_LEVEL);
unsetenv(rocprofsys::env_vars::LOG_FILE);
}
TEST_F(logger_test, logger_settings_monochrome)
{
setenv(rocprofsys::env_vars::MONOCHROME, "1", 1);
rocprofsys::logger_settings_t settings;
const auto* pattern = settings.get_log_pattern();
EXPECT_TRUE(std::string(pattern).find("%^") == std::string::npos);
EXPECT_TRUE(std::string(pattern).find("%$") == std::string::npos);
unsetenv(rocprofsys::env_vars::MONOCHROME);
}
TEST_F(logger_test, fork_child_creates_log_file_with_child_pid)
{
std::string test_log_base = "/tmp/test_fork_logger";
std::string test_log_ext = ".log";
std::string test_log_file = test_log_base + test_log_ext;
pid_t child_pid = fork();
if(child_pid == 0)
{
setenv(rocprofsys::env_vars::LOG_FILE, test_log_file.c_str(), 1);
pid_t current_pid = getpid();
auto& child_logger = rocprofsys::logger_t::instance();
std::string expected_child_log_file =
test_log_base + "_" + std::to_string(current_pid) + test_log_ext;
child_logger.info("Child process log entry");
child_logger.flush();
std::ifstream child_log(expected_child_log_file);
bool child_log_exists = child_log.good();
std::string content;
bool has_content = false;
if(child_log_exists)
{
std::getline(child_log, content);
has_content = content.find("Child process log entry") != std::string::npos;
}
std::remove(expected_child_log_file.c_str());
_exit(child_log_exists && has_content ? 0 : 1);
}
else
{
int status;
waitpid(child_pid, &status, 0);
int child_exit_code = WEXITSTATUS(status);
EXPECT_EQ(child_exit_code, 0)
<< "Child process failed to create its own log file with child PID";
}
}
TEST_F(logger_test, concurrent_logging_during_fork_no_deadlock)
{
auto& logger = rocprofsys::logger_t::instance();
constexpr int num_threads = 4;
constexpr int log_iterations = 200;
std::atomic<bool> keep_logging{ true };
std::vector<std::thread> threads;
threads.reserve(num_threads);
for(int i = 0; i < num_threads; ++i)
{
threads.emplace_back([&logger, &keep_logging, i] {
int iter = 0;
while(keep_logging.load(std::memory_order_relaxed))
{
logger.info("Thread {} iteration {}", i, iter++);
if(iter >= log_iterations) break;
}
});
}
int pipefd[2];
ASSERT_EQ(pipe(pipefd), 0) << "pipe() failed";
pid_t child_pid = fork();
if(child_pid == 0)
{
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
// In child: threads don't exist, logger was reset by atfork handler.
// Verify we can acquire a fresh logger without deadlocking.
auto& child_logger = rocprofsys::logger_t::instance();
child_logger.info("concurrent_fork_child_marker");
child_logger.flush();
_exit(0);
}
close(pipefd[1]);
auto child_output = read_fd(pipefd[0]);
close(pipefd[0]);
keep_logging.store(false, std::memory_order_relaxed);
for(auto& t : threads)
t.join();
int status;
pid_t waited = waitpid(child_pid, &status, 0);
ASSERT_NE(waited, -1) << "waitpid failed";
ASSERT_TRUE(WIFEXITED(status)) << "Child did not exit normally (possible deadlock)";
EXPECT_EQ(WEXITSTATUS(status), 0);
EXPECT_NE(child_output.find("concurrent_fork_child_marker"), std::string::npos)
<< "Child log message not found after concurrent fork. Got: " << child_output;
auto child_pid_marker = "P:" + std::to_string(child_pid);
EXPECT_NE(child_output.find(child_pid_marker), std::string::npos)
<< "Child PID not in log output. Expected P:" << child_pid
<< " in: " << child_output;
}
TEST_F(logger_test, multiple_sequential_forks)
{
constexpr int num_forks = 3;
for(int i = 0; i < num_forks; ++i)
{
int pipefd[2];
ASSERT_EQ(pipe(pipefd), 0) << "pipe() failed on fork " << i;
pid_t child_pid = fork();
if(child_pid == 0)
{
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
auto& child_logger = rocprofsys::logger_t::instance();
child_logger.info("sequential_fork_child_{}", i);
child_logger.flush();
bool valid = (child_logger.name() == "rocprofiler-systems");
_exit(valid ? 0 : 1);
}
close(pipefd[1]);
auto child_output = read_fd(pipefd[0]);
close(pipefd[0]);
int status;
waitpid(child_pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status)) << "Fork " << i << " child did not exit normally";
EXPECT_EQ(WEXITSTATUS(status), 0)
<< "Fork " << i << " child failed to reinitialize logger";
auto expected_marker = "sequential_fork_child_" + std::to_string(i);
EXPECT_NE(child_output.find(expected_marker), std::string::npos)
<< "Fork " << i << " child output missing marker. Got: " << child_output;
auto child_pid_marker = "P:" + std::to_string(child_pid);
EXPECT_NE(child_output.find(child_pid_marker), std::string::npos)
<< "Fork " << i << " child PID not in output. Got: " << child_output;
}
// Parent logger should still work after multiple forks
testing::internal::CaptureStdout();
auto& logger = rocprofsys::logger_t::instance();
EXPECT_EQ(logger.name(), "rocprofiler-systems");
logger.info("parent_after_{}_forks", num_forks);
logger.flush();
auto parent_output = testing::internal::GetCapturedStdout();
EXPECT_NE(parent_output.find("parent_after_3_forks"), std::string::npos)
<< "Parent log not captured after sequential forks. Got: " << parent_output;
}
TEST_F(logger_test, parent_continues_logging_after_fork)
{
std::string log_base = "/tmp/test_parent_post_fork";
std::string log_file = log_base + ".log";
setenv(rocprofsys::env_vars::LOG_FILE, log_file.c_str(), 1);
// Force a fresh logger with the file sink
// (the singleton was already created without a file sink,
// so we test via a child that gets a fresh logger)
pid_t child_pid = fork();
if(child_pid == 0)
{
auto& child_logger = rocprofsys::logger_t::instance();
child_logger.info("parent_post_fork_marker");
child_logger.flush();
pid_t current_pid = getpid();
std::string expected_log_file =
log_base + "_" + std::to_string(current_pid) + ".log";
std::ifstream log(expected_log_file);
bool found = false;
std::string line;
while(std::getline(log, line))
{
if(line.find("parent_post_fork_marker") != std::string::npos)
{
found = true;
break;
}
}
std::remove(expected_log_file.c_str());
_exit(found ? 0 : 1);
}
int status;
waitpid(child_pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), 0) << "Child failed to log after fork";
// Parent logger should remain functional
auto& parent_logger = rocprofsys::logger_t::instance();
EXPECT_NO_THROW(parent_logger.info("Parent logging after child completed"));
unsetenv(rocprofsys::env_vars::LOG_FILE);
}
TEST_F(logger_test, fork_child_gets_new_logger_instance)
{
auto& parent_logger = rocprofsys::logger_t::instance();
EXPECT_EQ(parent_logger.name(), "rocprofiler-systems");
EXPECT_FALSE(parent_logger.sinks().empty());
pid_t child_pid = fork();
if(child_pid == 0)
{
auto& child_logger = rocprofsys::logger_t::instance();
// The child logger should be freshly created with default sinks
// (stdout only, since ROCPROFSYS_LOG_FILE is not set)
bool has_sinks = !child_logger.sinks().empty();
bool has_name = (child_logger.name() == "rocprofiler-systems");
bool can_log = true;
try
{
child_logger.info("Verifying child logger works");
child_logger.flush();
} catch(...)
{
can_log = false;
}
_exit((has_sinks && has_name && can_log) ? 0 : 1);
}
int status;
waitpid(child_pid, &status, 0);
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), 0) << "Child should get a fresh, functional logger";
// Parent logger should remain valid after fork
EXPECT_EQ(parent_logger.name(), "rocprofiler-systems");
EXPECT_FALSE(parent_logger.sinks().empty());
EXPECT_NO_THROW(parent_logger.info("Parent still works after child fork"));
}
TEST_F(logger_test, concurrent_logging_stress_with_fork)
{
auto& logger = rocprofsys::logger_t::instance();
constexpr int num_threads = 8;
constexpr int iterations = 500;
constexpr int child_iterations = 10;
std::atomic<bool> start{ false };
std::atomic<int> ready_count{ 0 };
std::vector<std::thread> threads;
threads.reserve(num_threads);
for(int i = 0; i < num_threads; ++i)
{
threads.emplace_back([&logger, &start, &ready_count, i] {
ready_count.fetch_add(1, std::memory_order_release);
while(!start.load(std::memory_order_acquire))
{
}
for(int j = 0; j < iterations; ++j)
{
logger.info("Stress thread {} iter {}", i, j);
}
});
}
// Wait for all threads to be ready
while(ready_count.load(std::memory_order_acquire) < num_threads)
{
}
start.store(true, std::memory_order_release);
int pipefd[2];
ASSERT_EQ(pipe(pipefd), 0) << "pipe() failed";
// Fork while threads are actively logging
pid_t child_pid = fork();
if(child_pid == 0)
{
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
// Child: threads don't exist. Logger must be usable.
auto& child_logger = rocprofsys::logger_t::instance();
for(int j = 0; j < child_iterations; ++j)
{
child_logger.info("stress_child_iter_{}", j);
}
child_logger.flush();
_exit(0);
}
close(pipefd[1]);
auto child_output = read_fd(pipefd[0]);
close(pipefd[0]);
for(auto& t : threads)
t.join();
int status;
pid_t waited = waitpid(child_pid, &status, 0);
ASSERT_NE(waited, -1) << "waitpid failed";
ASSERT_TRUE(WIFEXITED(status))
<< "Child did not exit normally under stress (possible deadlock)";
EXPECT_EQ(WEXITSTATUS(status), 0);
// Validate child produced all expected log lines
for(int j = 0; j < child_iterations; ++j)
{
auto marker = "stress_child_iter_" + std::to_string(j);
EXPECT_NE(child_output.find(marker), std::string::npos)
<< "Missing child stress iteration " << j << " in output";
}
auto child_pid_marker = "P:" + std::to_string(child_pid);
EXPECT_NE(child_output.find(child_pid_marker), std::string::npos)
<< "Child PID not in stress test output. Got: " << child_output;
// Parent logger should still work
EXPECT_NO_THROW(logger.info("Parent done after stress fork"));
}