Added a Process Isolated Test Runner (#1993)

* Added single process isolation support to execute tests

* Address review comments

* Update README

* Removed requirement of explicit call to clear method

* Added macros for simplified usage

* Updated tests to use process isolation framework

* Adjust summary output format for isolated tests

* Updated rccl_wrap tests

* Used process isolation in AllocTests

* Used process isolation and fixed failing tests

* Modified test output, added signal handling

Updated macros to handle lambdas

* Convert argcheck tests to isolated tests

* Convert proxy tests to isolated tests

* Remove non-supported test

* Fixed file descriptor handling and clearing env vars for tests
This commit is contained in:
Atul Kulkarni
2025-12-08 10:36:05 -06:00
committed by GitHub
orang tua 29e1567b95
melakukan 7e10267dfd
11 mengubah file dengan 5275 tambahan dan 3210 penghapusan
@@ -0,0 +1,696 @@
/*************************************************************************
* Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
#include "ProcessIsolatedTestRunner.hpp"
#include <errno.h>
#include <fcntl.h>
#include <gtest/gtest.h>
#include <unistd.h>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <thread>
#include "ErrCode.hpp"
namespace RcclUnitTesting
{
// Exit codes for test process results
enum RcclTestCode
{
RCCL_TEST_INVALID = -1,
RCCL_TEST_SUCCESS = 0,
RCCL_TEST_FAILURE = 1,
RCCL_TEST_UNKNOWN_EXCEPTION = 2,
RCCL_TEST_TIMEOUT = 3,
RCCL_TEST_SKIPPED = 4
};
// Define static members
std::mutex ProcessIsolatedTestRunner::testConfigsMutex_;
std::vector<ProcessIsolatedTestRunner::TestConfig> ProcessIsolatedTestRunner::testConfigs_;
std::mutex ProcessIsolatedTestRunner::resultsMutex_;
std::vector<ProcessIsolatedTestRunner::TestResult> ProcessIsolatedTestRunner::testResults_;
// TestResult implementation
ProcessIsolatedTestRunner::TestResult::TestResult()
: passed(false), skipped(false), exitCode(-1), processId(-1), duration(0)
{}
// TestConfig implementation
ProcessIsolatedTestRunner::TestConfig::TestConfig(
const std::string& testName, std::function<void()> logic
)
: name(testName), testLogic(logic), timeout(30), inheritParentEnv(true)
{}
ProcessIsolatedTestRunner::TestConfig& ProcessIsolatedTestRunner::TestConfig::withEnvironment(
const std::unordered_map<std::string, std::string>& env
)
{
environmentVariables = env;
return *this;
}
ProcessIsolatedTestRunner::TestConfig&
ProcessIsolatedTestRunner::TestConfig::withTimeout(std::chrono::seconds timeoutSeconds)
{
timeout = timeoutSeconds;
return *this;
}
ProcessIsolatedTestRunner::TestConfig&
ProcessIsolatedTestRunner::TestConfig::withCleanEnvironment(bool inherit)
{
inheritParentEnv = inherit;
return *this;
}
ProcessIsolatedTestRunner::TestConfig&
ProcessIsolatedTestRunner::TestConfig::clearVariable(const std::string& varName)
{
clearEnvVars.push_back(varName);
return *this;
}
ProcessIsolatedTestRunner::TestConfig& ProcessIsolatedTestRunner::TestConfig::setVariable(
const std::string& name, const std::string& value
)
{
environmentVariables[name] = value;
return *this;
}
// ExecutionOptions implementation
ProcessIsolatedTestRunner::ExecutionOptions::ExecutionOptions()
: stopOnFirstFailure(false), verboseLogging(true)
{}
// Apply environment variables to current process
void ProcessIsolatedTestRunner::applyEnvironmentVariables(const TestConfig& config)
{
// Clear specified environment variables first
for(const auto& varName : config.clearEnvVars)
{
unsetenv(varName.c_str());
}
// If not inheriting parent environment, clear all environment variables
if(!config.inheritParentEnv)
{
// Clear all existing environment variables
if(clearenv() != 0)
{
std::cerr << "Warning: Failed to clear environment variables" << std::endl;
}
// Set only the specified variables
for(const auto& [name, value] : config.environmentVariables)
{
setenv(name.c_str(), value.c_str(), 1);
}
}
else
{
// Just set/override the specified variables
for(const auto& [name, value] : config.environmentVariables)
{
setenv(name.c_str(), value.c_str(), 1);
}
}
}
// Execute a single test in a separate process
int ProcessIsolatedTestRunner::runTestInProcess(const TestConfig& config)
{
pid_t processId = getpid();
if(config.name.empty())
{
std::cerr << "Error: Test name is empty for process " << processId << std::endl;
return RCCL_TEST_FAILURE;
}
try
{
// Apply environment variables
applyEnvironmentVariables(config);
// Thread-safe test execution with timeout protection
std::atomic<bool> testCompleted{false};
std::exception_ptr testException = nullptr;
bool testPassed = true;
bool testSkipped = false;
// Run test in a separate thread to allow timeout handling
std::thread testThread(
[&]()
{
try
{
// Get initial test state
const ::testing::UnitTest* unitTest = ::testing::UnitTest::GetInstance();
size_t initialFailureCount = unitTest->failed_test_count();
size_t initialSkippedCount = unitTest->skipped_test_count();
// Execute the test logic
config.testLogic();
// Check if any new test failures occurred
size_t finalFailureCount = unitTest->failed_test_count();
size_t finalSkippedCount = unitTest->skipped_test_count();
testPassed = (finalFailureCount == initialFailureCount);
testSkipped = (finalSkippedCount > initialSkippedCount);
testCompleted = true;
}
catch(...)
{
testException = std::current_exception();
testPassed = false;
testCompleted = true;
}
}
);
// Wait for test completion with timeout
auto start = std::chrono::steady_clock::now();
const auto timeout = config.timeout;
while(!testCompleted.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if(std::chrono::steady_clock::now() - start > timeout)
{
// Test timed out
INFO(
"Test '%s' TIMED OUT after %ld seconds\n",
config.name.c_str(),
timeout.count()
);
fflush(NULL);
testThread.detach();
return RCCL_TEST_TIMEOUT;
}
}
// Wait for thread completion
if(testThread.joinable())
{
testThread.join();
}
// Check if test threw an exception
if(testException)
{
std::rethrow_exception(testException);
}
// Flush output before returning (needed before _exit())
fflush(NULL);
// Return appropriate exit code based on test result
if(testSkipped)
{
return RCCL_TEST_SKIPPED;
}
else if(testPassed)
{
return RCCL_TEST_SUCCESS;
}
else
{
return RCCL_TEST_FAILURE;
}
}
catch(const std::exception& e)
{
INFO("Test '%s' FAILED with exception: %s\n", config.name.c_str(), e.what());
std::cerr << "Exception in test '" << config.name << "': " << e.what() << std::endl;
fflush(NULL);
return RCCL_TEST_FAILURE;
}
catch(...)
{
INFO("Test '%s' FAILED with unknown exception\n", config.name.c_str());
std::cerr << "Unknown exception in test '" << config.name << "'" << std::endl;
fflush(NULL);
return RCCL_TEST_UNKNOWN_EXCEPTION;
}
}
// Register a test configuration
void ProcessIsolatedTestRunner::registerTest(const TestConfig& config)
{
std::lock_guard<std::mutex> lock(testConfigsMutex_);
testConfigs_.push_back(config);
}
// Register a simple test with just name and logic
void ProcessIsolatedTestRunner::registerTest(
const std::string& name, std::function<void()> testLogic
)
{
registerTest(TestConfig(name, testLogic));
}
// Register a test with environment variables
void ProcessIsolatedTestRunner::registerTest(
const std::string& name,
std::function<void()> testLogic,
const std::unordered_map<std::string, std::string>& env
)
{
registerTest(TestConfig(name, testLogic).withEnvironment(env));
}
// Record test result (thread-safe)
void ProcessIsolatedTestRunner::recordTestResult(const TestResult& result)
{
std::lock_guard<std::mutex> lock(resultsMutex_);
testResults_.push_back(result);
}
// Helper method: Create pipes for capturing process output
bool ProcessIsolatedTestRunner::createOutputPipes(int stdoutPipe[2], int stderrPipe[2])
{
// Create pipes for stdout and stderr
// stdoutPipe[0] = read end, stdoutPipe[1] = write end
if(pipe(stdoutPipe) == -1)
{
std::cerr << "Failed to create stdout pipe: " << strerror(errno) << std::endl;
return false;
}
if(pipe(stderrPipe) == -1)
{
std::cerr << "Failed to create stderr pipe: " << strerror(errno) << std::endl;
close(stdoutPipe[0]);
close(stdoutPipe[1]);
return false;
}
return true;
}
// Helper method: Redirect child process output to pipes
void ProcessIsolatedTestRunner::redirectOutputToPipes(int stdoutPipe[2], int stderrPipe[2])
{
// Close read ends of pipes in child process (not needed)
close(stdoutPipe[0]);
close(stderrPipe[0]);
// Redirect stdout and stderr to write ends of pipes
dup2(stdoutPipe[1], STDOUT_FILENO);
dup2(stderrPipe[1], STDERR_FILENO);
// Close the original write end file descriptors after duplication
// The duplicated descriptors (STDOUT_FILENO, STDERR_FILENO) will be closed by _exit()
close(stdoutPipe[1]);
close(stderrPipe[1]);
}
// Helper method: Capture output from child process pipes
ProcessIsolatedTestRunner::CapturedOutput ProcessIsolatedTestRunner::captureProcessOutput(
int stdoutPipe[2], int stderrPipe[2], pid_t pid, int* status
)
{
// Close write ends of pipes in parent process (not needed)
close(stdoutPipe[1]);
close(stderrPipe[1]);
CapturedOutput output;
char buffer[4096];
ssize_t count;
// Read from stdout pipe
while((count = read(stdoutPipe[0], buffer, sizeof(buffer) - 1)) > 0)
{
buffer[count] = '\0';
output.stdoutContent += buffer;
}
close(stdoutPipe[0]);
// Read from stderr pipe
while((count = read(stderrPipe[0], buffer, sizeof(buffer) - 1)) > 0)
{
buffer[count] = '\0';
output.stderrContent += buffer;
}
close(stderrPipe[0]);
// Wait for child to exit (blocking)
waitpid(pid, status, 0);
return output;
}
// Helper method: Display captured output
void ProcessIsolatedTestRunner::displayCapturedOutput(
const CapturedOutput& output, const std::string& testName
)
{
if(!output.stdoutContent.empty())
{
std::cout << output.stdoutContent;
if(output.stdoutContent.back() != '\n')
std::cout << '\n';
}
if(!output.stderrContent.empty())
{
std::cerr << output.stderrContent;
if(output.stderrContent.back() != '\n')
std::cerr << '\n';
}
}
// Execute all registered tests (simplified sequential execution only)
bool ProcessIsolatedTestRunner::executeAllTests(const ExecutionOptions& options)
{
// Get test configurations to run
std::vector<TestConfig> testsToRun;
{
std::lock_guard<std::mutex> lock(testConfigsMutex_);
testsToRun = testConfigs_;
}
// Clear previous results
{
std::lock_guard<std::mutex> lock(resultsMutex_);
testResults_.clear();
}
// Sequential execution
for(const auto& testConfig : testsToRun)
{
auto startTime = std::chrono::steady_clock::now();
int stdout_fd[2], stderr_fd[2];
if(!createOutputPipes(stdout_fd, stderr_fd))
{
std::cerr << "Failed to create output files for test '" << testConfig.name << "'"
<< std::endl;
continue;
}
pid_t pid = fork();
if(pid == 0)
{
redirectOutputToPipes(stdout_fd, stderr_fd);
int result = runTestInProcess(testConfig);
// Use _exit() instead of exit() to avoid atexit handlers
// This prevents GPU runtime cleanup issues after fork
_exit(result);
}
else if(pid > 0)
{
// Log test start with environment variables if any
if(!testConfig.environmentVariables.empty())
{
std::string envVars;
for(const auto& [name, value] : testConfig.environmentVariables)
{
if(!envVars.empty())
envVars += ", ";
envVars += name + "=" + value;
}
INFO(
"Running isolated test '%s' (PID: %d) with env: %s\n",
testConfig.name.c_str(),
pid,
envVars.c_str()
);
}
else
{
INFO("Running isolated test '%s' (PID: %d)\n", testConfig.name.c_str(), pid);
}
int status;
CapturedOutput output = captureProcessOutput(stdout_fd, stderr_fd, pid, &status);
auto endTime = std::chrono::steady_clock::now();
auto duration
= std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
TestResult testResult;
testResult.testName = testConfig.name;
testResult.processId = pid;
testResult.duration = duration;
if(WIFEXITED(status))
{
int exitCode = WEXITSTATUS(status);
testResult.exitCode = exitCode;
testResult.passed = (exitCode == RCCL_TEST_SUCCESS);
testResult.skipped = (exitCode == RCCL_TEST_SKIPPED);
if(exitCode == RCCL_TEST_SUCCESS)
{
INFO("Test '%s' PASSED (%ld ms)\n", testConfig.name.c_str(), duration.count());
}
else if(exitCode == RCCL_TEST_TIMEOUT)
{
INFO(
"Test '%s' (PID: %d) TIMED OUT after %ld ms\n",
testConfig.name.c_str(),
pid,
duration.count()
);
testResult.errorMessage = "Test timed out";
}
else if(exitCode == RCCL_TEST_SKIPPED)
{
INFO(
"Test '%s' (PID: %d) SKIPPED in %ld ms\n",
testConfig.name.c_str(),
pid,
duration.count()
);
testResult.errorMessage = "Test skipped";
}
else
{
INFO(
"Test '%s' (PID: %d) FAILED with exit code %d after %ld ms\n",
testConfig.name.c_str(),
pid,
exitCode,
duration.count()
);
testResult.errorMessage
= "Test failed with exit code " + std::to_string(exitCode);
}
}
else if(WIFSIGNALED(status))
{
int signal = WTERMSIG(status);
// Check if test reported success before signal termination
bool testPassed = (output.stdoutContent.find("PASSED") != std::string::npos);
if(testPassed)
{
// Test completed successfully before signal (e.g., GPU runtime cleanup)
testResult.passed = true;
testResult.skipped = false;
testResult.exitCode = RCCL_TEST_SUCCESS;
INFO("Test '%s' PASSED (%ld ms)\n", testConfig.name.c_str(), duration.count());
}
else
{
// Test terminated by signal before completion (crash)
testResult.passed = false;
testResult.skipped = false;
testResult.exitCode = -signal;
testResult.errorMessage = "Terminated by signal " + std::to_string(signal);
INFO(
"Test '%s' (PID: %d) terminated by signal %d after %ld ms\n",
testConfig.name.c_str(),
pid,
signal,
duration.count()
);
}
}
else
{
testResult.passed = false;
testResult.skipped = false;
testResult.exitCode = RCCL_TEST_INVALID;
testResult.errorMessage = "Failed to wait for process";
}
displayCapturedOutput(output, testConfig.name);
recordTestResult(testResult);
// Stop on first failure if requested
if(options.stopOnFirstFailure && !testResult.passed && !testResult.skipped)
{
break;
}
}
else
{
// Fork failed
TestResult testResult;
testResult.testName = testConfig.name;
testResult.passed = false;
testResult.skipped = false;
testResult.exitCode = RCCL_TEST_INVALID;
testResult.processId = RCCL_TEST_INVALID;
testResult.duration = std::chrono::milliseconds(0);
testResult.errorMessage = "Failed to fork process";
recordTestResult(testResult);
INFO("Failed to fork process for test '%s'\n", testConfig.name.c_str());
if(options.stopOnFirstFailure)
{
break;
}
}
}
bool result = generateReport(options);
// Automatically clear test configurations and results after execution
// This ensures a clean state for the next test suite without requiring
// explicit clear() calls from test cases
{
std::lock_guard<std::mutex> lock(testConfigsMutex_);
testConfigs_.clear();
}
{
std::lock_guard<std::mutex> lock(resultsMutex_);
testResults_.clear();
}
return result;
}
// Generate and display test report
bool ProcessIsolatedTestRunner::generateReport(const ExecutionOptions& options)
{
int totalTests = 0;
int passedTests = 0;
int failedTests = 0;
int skippedTests = 0;
std::chrono::milliseconds totalDuration{0};
{
std::lock_guard<std::mutex> lock(resultsMutex_);
totalTests = testResults_.size();
for(const auto& result : testResults_)
{
if(result.skipped)
{
skippedTests++;
}
else if(result.passed)
{
passedTests++;
}
else
{
failedTests++;
}
totalDuration += result.duration;
}
}
// Report summary only if there are failures or multiple tests
if(failedTests > 0 || totalTests > 1)
{
INFO(
"Process-Isolated Tests: %d passed, %d failed, %d skipped (%ld ms total)\n",
passedTests,
failedTests,
skippedTests,
totalDuration.count()
);
if(failedTests > 0)
{
std::lock_guard<std::mutex> lock(resultsMutex_);
for(const auto& result : testResults_)
{
if(!result.passed && !result.skipped)
{
INFO(
" Failed: %s - %s\n",
result.testName.c_str(),
result.errorMessage.c_str()
);
}
}
}
}
return failedTests == 0;
}
// Get detailed test results (thread-safe)
std::vector<ProcessIsolatedTestRunner::TestResult> ProcessIsolatedTestRunner::getTestResults()
{
std::lock_guard<std::mutex> lock(resultsMutex_);
return testResults_;
}
// Clear test registry and results (thread-safe)
void ProcessIsolatedTestRunner::clear()
{
size_t registeredCount = 0;
size_t executedCount = 0;
// Check for unexecuted tests before clearing
{
std::lock_guard<std::mutex> lock(testConfigsMutex_);
registeredCount = testConfigs_.size();
}
{
std::lock_guard<std::mutex> lock(resultsMutex_);
executedCount = testResults_.size();
}
// Warn if tests were registered but not all executed
if(registeredCount > 0 && executedCount < registeredCount)
{
std::cerr << "\n⚠️ WARNING: ProcessIsolatedTestRunner::clear() called with "
<< (registeredCount - executedCount) << " unexecuted test(s)!\n"
<< " Registered: " << registeredCount << " test(s)\n"
<< " Executed: " << executedCount << " test(s)\n"
<< " Did you forget to call executeAllTests()?\n"
<< std::endl;
}
// Clear the registrations and results
{
std::lock_guard<std::mutex> lock(testConfigsMutex_);
testConfigs_.clear();
}
{
std::lock_guard<std::mutex> lock(resultsMutex_);
testResults_.clear();
}
}
// Get number of registered tests
size_t ProcessIsolatedTestRunner::getTestCount()
{
std::lock_guard<std::mutex> lock(testConfigsMutex_);
return testConfigs_.size();
}
} // namespace RcclUnitTesting
@@ -0,0 +1,365 @@
/*************************************************************************
* Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
#pragma once
#include <sys/wait.h>
#include <unistd.h>
#include <chrono>
#include <cstdlib>
#include <functional>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace RcclUnitTesting
{
/**
* @brief Generic thread-safe process isolated test runner
*
* This class provides a framework for running tests in isolated processes
* with clean environment settings and sequential execution.
*
*/
class ProcessIsolatedTestRunner
{
public:
/**
* @brief Test execution result structure
*/
struct TestResult
{
std::string testName; ///< Name of the test
bool passed; ///< Whether the test passed
bool skipped; ///< Whether the test skipped
int exitCode; ///< Process exit code
pid_t processId; ///< Process ID that ran the test
std::chrono::milliseconds duration; ///< Test execution duration
std::string errorMessage; ///< Error message if test failed
std::unordered_map<std::string, std::string> environment; ///< Environment variables used
/**
* @brief Default constructor
*/
TestResult();
};
/**
* @brief Test configuration structure
*/
struct TestConfig
{
std::string name; ///< Test name
std::function<void()> testLogic; ///< Test function to execute
std::unordered_map<std::string, std::string>
environmentVariables; ///< Environment variables to set
std::chrono::seconds timeout; ///< Test timeout
bool inheritParentEnv; ///< Whether to inherit parent environment
std::vector<std::string> clearEnvVars; ///< Environment variables to explicitly clear
/**
* @brief Constructor
* @param testName Name of the test
* @param logic Test function to execute
*/
TestConfig(const std::string& testName, std::function<void()> logic);
/**
* @brief Set environment variables for this test
* @param env Map of environment variable name-value pairs
* @return Reference to this TestConfig for method chaining
*/
TestConfig& withEnvironment(const std::unordered_map<std::string, std::string>& env);
/**
* @brief Set timeout for this test
* @param timeoutSeconds Timeout in seconds
* @return Reference to this TestConfig for method chaining
*/
TestConfig& withTimeout(std::chrono::seconds timeoutSeconds);
/**
* @brief Configure environment inheritance
* @param inherit Whether to inherit parent environment variables
* @return Reference to this TestConfig for method chaining
*/
TestConfig& withCleanEnvironment(bool inherit = false);
/**
* @brief Clear a specific environment variable
* @param varName Name of the variable to clear
* @return Reference to this TestConfig for method chaining
*/
TestConfig& clearVariable(const std::string& varName);
/**
* @brief Set a specific environment variable
* @param name Variable name
* @param value Variable value
* @return Reference to this TestConfig for method chaining
*/
TestConfig& setVariable(const std::string& name, const std::string& value);
};
/**
* @brief Execution options for test runner
*/
struct ExecutionOptions
{
bool stopOnFirstFailure; ///< Stop execution on first test failure
bool verboseLogging; ///< Enable verbose logging
/**
* @brief Default constructor with sensible defaults
*/
ExecutionOptions();
};
private:
/**
* @brief Structure to hold captured process output
*/
struct CapturedOutput
{
std::string stdoutContent; ///< Captured stdout content
std::string stderrContent; ///< Captured stderr content
};
// Thread-safe static members for test management
static std::mutex testConfigsMutex_;
static std::vector<TestConfig> testConfigs_;
static std::mutex resultsMutex_;
static std::vector<TestResult> testResults_;
/**
* @brief Apply environment variables to current process
* @param config Test configuration containing environment settings
*/
static void applyEnvironmentVariables(const TestConfig& config);
/**
* @brief Execute a single test in the child process
* @param config Test configuration
* @return Exit code (0 for success, non-zero for failure)
*/
static int runTestInProcess(const TestConfig& config);
/**
* @brief Create pipes for capturing process output
* @param stdoutPipe Array to hold stdout pipe file descriptors [read, write]
* @param stderrPipe Array to hold stderr pipe file descriptors [read, write]
* @return True if pipes were created successfully, false otherwise
*/
static bool createOutputPipes(int stdoutPipe[2], int stderrPipe[2]);
/**
* @brief Redirect child process output to pipes
* @param stdoutPipe Stdout pipe file descriptors [read, write]
* @param stderrPipe Stderr pipe file descriptors [read, write]
*/
static void redirectOutputToPipes(int stdoutPipe[2], int stderrPipe[2]);
/**
* @brief Capture output from child process via pipes
* @param stdoutPipe Stdout pipe file descriptors [read, write]
* @param stderrPipe Stderr pipe file descriptors [read, write]
* @param pid Child process ID to monitor
* @param status Pointer to status variable for waitpid
* @return Captured output from stdout and stderr
*/
static CapturedOutput
captureProcessOutput(int stdoutPipe[2], int stderrPipe[2], pid_t pid, int* status);
/**
* @brief Display captured output with formatted delimiters
* @param output Captured output to display
* @param testName Name of the test for context
*/
static void displayCapturedOutput(const CapturedOutput& output, const std::string& testName);
public:
/**
* @brief Register a test configuration
* @param config Complete test configuration
*/
static void registerTest(const TestConfig& config);
/**
* @brief Register a simple test with just name and logic
* @param name Test name
* @param testLogic Test function to execute
*/
static void registerTest(const std::string& name, std::function<void()> testLogic);
/**
* @brief Register a test with environment variables
* @param name Test name
* @param testLogic Test function to execute
* @param env Environment variables to set for this test
*/
static void registerTest(
const std::string& name,
std::function<void()> testLogic,
const std::unordered_map<std::string, std::string>& env
);
/**
* @brief Record a test result (thread-safe)
* @param result Test result to record
*/
static void recordTestResult(const TestResult& result);
/**
* @brief Execute all registered tests sequentially
* @param options Execution options (defaults to continue on failure)
* @return True if all tests passed, false otherwise
* @note This method automatically clears all test registrations and results
* after execution, ensuring a clean state for the next test suite.
*/
static bool executeAllTests(const ExecutionOptions& options = ExecutionOptions());
/**
* @brief Generate and display test report
* @param options Execution options used for the test run
* @return True if all tests passed, false otherwise
*/
static bool generateReport(const ExecutionOptions& options);
/**
* @brief Get detailed test results (thread-safe)
* @return Vector of all test results
*/
static std::vector<TestResult> getTestResults();
/**
* @brief Clear test registry and results (thread-safe)
* @note Calling this method manually is typically not necessary, as
* executeAllTests() automatically clears registrations after execution.
* This method is primarily useful for advanced use cases or when tests
* are registered but not executed.
*/
static void clear();
/**
* @brief Get number of registered tests
* @return Number of registered tests
*/
static size_t getTestCount();
};
// Macros for Simplified Usage
/**
* @brief Register and execute a single isolated test with minimal boilerplate
*
* Uses variadic macros to automatically handle commas in lambda bodies
*
* @param test_name Name of the test (string)
* @param ... Lambda containing test logic (variadic to handle internal commas)
*
* Example:
* RUN_ISOLATED_TEST("MyTest", []() {
* EXPECT_TRUE(someFunction());
* });
*/
#define RUN_ISOLATED_TEST(test_name, ...) \
do \
{ \
::RcclUnitTesting::ProcessIsolatedTestRunner::registerTest(test_name, __VA_ARGS__); \
bool passed_ = ::RcclUnitTesting::ProcessIsolatedTestRunner::executeAllTests(); \
EXPECT_TRUE(passed_) << "Isolated test '" << test_name << "' failed"; \
} \
while(0)
/**
* @brief Register and execute a single isolated test with environment variables
*
* Uses variadic macros to automatically handle environment variable initializer lists
*
* @param test_name Name of the test (string)
* @param test_body Lambda containing test logic
* @param ... Environment variables as initializer list
*
* Example:
* RUN_ISOLATED_TEST_WITH_ENV("MyTest",
* []() { EXPECT_TRUE(someFunction()); },
* {{"VAR1", "value1"}, {"VAR2", "value2"}});
*
* Note: Uses __VA_ARGS__ to capture environment variables, which automatically
* handles commas in the initializer list without requiring extra parentheses.
*/
#define RUN_ISOLATED_TEST_WITH_ENV(test_name, test_body, ...) \
do \
{ \
::RcclUnitTesting::ProcessIsolatedTestRunner::registerTest( \
test_name, \
test_body, \
__VA_ARGS__ \
); \
bool passed_ = ::RcclUnitTesting::ProcessIsolatedTestRunner::executeAllTests(); \
EXPECT_TRUE(passed_) << "Isolated test '" << test_name << "' failed"; \
} \
while(0)
/**
* @brief Register and execute multiple isolated tests with default options
*
* This macro takes multiple TestConfig objects and executes them all.
* Tests are automatically cleaned up after execution.
*
* Example:
* RUN_ISOLATED_TESTS(
* ProcessIsolatedTestRunner::TestConfig("Test1", []() { ... }),
* ProcessIsolatedTestRunner::TestConfig("Test2", []() { ... })
* .withEnvironment({{"VAR", "value"}}),
* ProcessIsolatedTestRunner::TestConfig("Test3", []() { ... })
* .withTimeout(std::chrono::seconds(60))
* );
*/
#define RUN_ISOLATED_TESTS(...) \
do \
{ \
::RcclUnitTesting::ProcessIsolatedTestRunner::TestConfig configs_[] = {__VA_ARGS__}; \
for(const auto& config_ : configs_) \
{ \
::RcclUnitTesting::ProcessIsolatedTestRunner::registerTest(config_); \
} \
bool passed_ = ::RcclUnitTesting::ProcessIsolatedTestRunner::executeAllTests(); \
EXPECT_TRUE(passed_) << "One or more isolated tests failed"; \
} \
while(0)
/**
* @brief Register and execute multiple isolated tests with custom options
*
* This macro takes execution options and multiple TestConfig objects.
*
* Example:
* ProcessIsolatedTestRunner::ExecutionOptions opts;
* opts.stopOnFirstFailure = true;
* opts.verboseLogging = true;
*
* RUN_ISOLATED_TESTS_WITH_OPTIONS(opts,
* ProcessIsolatedTestRunner::TestConfig("Test1", []() { ... }),
* ProcessIsolatedTestRunner::TestConfig("Test2", []() { ... })
* );
*/
#define RUN_ISOLATED_TESTS_WITH_OPTIONS(options, ...) \
do \
{ \
::RcclUnitTesting::ProcessIsolatedTestRunner::TestConfig configs_[] = {__VA_ARGS__}; \
for(const auto& config_ : configs_) \
{ \
::RcclUnitTesting::ProcessIsolatedTestRunner::registerTest(config_); \
} \
bool passed_ = ::RcclUnitTesting::ProcessIsolatedTestRunner::executeAllTests(options); \
EXPECT_TRUE(passed_) << "One or more isolated tests failed"; \
} \
while(0)
} // namespace RcclUnitTesting
File diff ditekan karena terlalu besar Load Diff