Files
rustytorch/integration_tests/src/common.rs
T
2026-03-04 00:08:42 +00:00

536 lines
16 KiB
Rust

/*!
Common utilities and infrastructure for integration tests.
This module provides shared functionality used across all integration test categories,
including test environment setup, resource management, and utility functions.
*/
#![cfg(feature = "integration-tests")]
/*!
*/
use anyhow::Result;
use std::time::{Duration, Instant};
use tracing::{info, warn, error, debug};
use tokio::time::timeout;
use uuid::Uuid;
use sysinfo::SystemExt;
// Re-export stubs for easy access from test files
pub use crate::stubs::*;
/// Test execution context with resource tracking and cleanup
pub struct TestContext {
pub test_id: Uuid,
pub start_time: Instant,
pub allocated_resources: Vec<AllocatedResource>,
pub temp_files: Vec<std::path::PathBuf>,
pub spawned_processes: Vec<tokio::process::Child>,
}
impl Default for TestContext {
fn default() -> Self {
Self::new()
}
}
impl TestContext {
pub fn new() -> Self {
Self {
test_id: Uuid::new_v4(),
start_time: Instant::now(),
allocated_resources: Vec::new(),
temp_files: Vec::new(),
spawned_processes: Vec::new(),
}
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
pub fn add_resource(&mut self, resource: AllocatedResource) {
self.allocated_resources.push(resource);
}
pub fn add_temp_file(&mut self, path: std::path::PathBuf) {
self.temp_files.push(path);
}
pub fn add_process(&mut self, child: tokio::process::Child) {
self.spawned_processes.push(child);
}
/// Clean up all allocated resources
pub async fn cleanup(&mut self) -> Result<()> {
debug!("Cleaning up test context {}", self.test_id);
// Kill spawned processes
for mut child in self.spawned_processes.drain(..) {
if let Err(e) = child.kill().await {
warn!("Failed to kill process: {}", e);
}
}
// Clean up temporary files
for path in self.temp_files.drain(..) {
if let Err(e) = std::fs::remove_file(&path) {
warn!("Failed to remove temp file {:?}: {}", path, e);
}
}
// Clean up allocated resources
for resource in self.allocated_resources.drain(..) {
if let Err(e) = resource.cleanup().await {
warn!("Failed to cleanup resource: {}", e);
}
}
debug!("Test context cleanup completed in {:?}", self.elapsed());
Ok(())
}
}
impl Drop for TestContext {
fn drop(&mut self) {
if !self.allocated_resources.is_empty() || !self.temp_files.is_empty() || !self.spawned_processes.is_empty() {
warn!("TestContext dropped without cleanup! Resources may leak.");
}
}
}
/// Represents an allocated resource that needs cleanup
#[derive(Debug)]
pub enum AllocatedResource {
MemoryBuffer { size_bytes: usize, device_id: usize },
GpuContext { device_id: usize },
NetworkPort { port: u16 },
DatabaseConnection { connection_id: String },
Container { container_id: String },
TempDirectory { path: std::path::PathBuf },
}
impl AllocatedResource {
pub async fn cleanup(self) -> Result<()> {
match self {
Self::MemoryBuffer { size_bytes, device_id } => {
debug!("Cleaning up memory buffer: {} bytes on device {}", size_bytes, device_id);
// Actual memory cleanup would happen through the allocator
Ok(())
}
Self::GpuContext { device_id } => {
debug!("Cleaning up GPU context on device {}", device_id);
// GPU context cleanup
Ok(())
}
Self::NetworkPort { port } => {
debug!("Released network port {}", port);
Ok(())
}
Self::DatabaseConnection { connection_id } => {
debug!("Closing database connection {}", connection_id);
Ok(())
}
Self::Container { container_id } => {
debug!("Stopping container {}", container_id);
let output = tokio::process::Command::new("docker")
.args(["stop", &container_id])
.output()
.await?;
if !output.status.success() {
warn!("Failed to stop container {}: {}",
container_id, String::from_utf8_lossy(&output.stderr));
}
// Remove container
let output = tokio::process::Command::new("docker")
.args(["rm", &container_id])
.output()
.await?;
if !output.status.success() {
warn!("Failed to remove container {}: {}",
container_id, String::from_utf8_lossy(&output.stderr));
}
Ok(())
}
Self::TempDirectory { path } => {
debug!("Removing temporary directory {:?}", path);
if let Err(e) = std::fs::remove_dir_all(&path) {
warn!("Failed to remove temp directory {:?}: {}", path, e);
}
Ok(())
}
}
}
}
/// System resource monitor for tracking usage during tests
pub struct ResourceMonitor {
initial_memory: u64,
initial_gpu_memory: Vec<u64>,
start_time: Instant,
}
impl ResourceMonitor {
pub fn new() -> Result<Self> {
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let initial_memory = sys.used_memory();
let initial_gpu_memory = Vec::new(); // Would query GPU memory here
Ok(Self {
initial_memory,
initial_gpu_memory,
start_time: Instant::now(),
})
}
pub fn snapshot(&self) -> ResourceSnapshot {
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let current_memory = sys.used_memory();
ResourceSnapshot {
timestamp: Instant::now(),
memory_used_mb: current_memory / 1024 / 1024,
memory_delta_mb: ((current_memory as i64) - (self.initial_memory as i64)) / 1024 / 1024,
gpu_memory_used_mb: Vec::new(), // Would query GPU memory here
elapsed: self.start_time.elapsed(),
}
}
}
#[derive(Debug, Clone)]
pub struct ResourceSnapshot {
pub timestamp: Instant,
pub memory_used_mb: u64,
pub memory_delta_mb: i64,
pub gpu_memory_used_mb: Vec<u64>,
pub elapsed: Duration,
}
impl ResourceSnapshot {
pub fn print_summary(&self) {
info!("Resource snapshot at {:?}:", self.elapsed);
info!(" Memory: {} MB (Δ{:+} MB)", self.memory_used_mb, self.memory_delta_mb);
if !self.gpu_memory_used_mb.is_empty() {
for (i, gpu_mem) in self.gpu_memory_used_mb.iter().enumerate() {
info!(" GPU {}: {} MB", i, gpu_mem);
}
}
}
}
/// Network utilities for distributed testing
pub struct NetworkUtils;
impl NetworkUtils {
/// Find an available port for testing
pub async fn find_available_port() -> Result<u16> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
Ok(port)
}
/// Check if a service is reachable
pub async fn wait_for_service(host: &str, port: u16, timeout_secs: u64) -> Result<()> {
let timeout_duration = Duration::from_secs(timeout_secs);
let start = Instant::now();
while start.elapsed() < timeout_duration {
if let Ok(_) = tokio::net::TcpStream::connect((host, port)).await {
return Ok(());
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
anyhow::bail!("Service at {host}:{port} not reachable within {timeout_duration:?}")
}
/// Get local IP address for multi-node testing
pub fn get_local_ip() -> Result<String> {
let local_ip = local_ip_address::local_ip()?;
Ok(local_ip.to_string())
}
}
/// Container management utilities
pub struct ContainerManager {
runtime: String,
}
impl ContainerManager {
pub fn new(runtime: String) -> Self {
Self { runtime }
}
/// Start a container and return its ID
pub async fn start_container(
&self,
image: &str,
ports: &[u16],
env_vars: &[(&str, &str)],
volumes: &[(&str, &str)],
) -> Result<String> {
let mut args: Vec<String> = vec!["run".to_string(), "-d".to_string()];
// Add port mappings
for port in ports {
args.push("-p".to_string());
args.push(format!("{port}:{port}"));
}
// Add environment variables
for (key, value) in env_vars {
args.push("-e".to_string());
args.push(format!("{key}={value}"));
}
// Add volume mounts
for (host_path, container_path) in volumes {
args.push("-v".to_string());
args.push(format!("{host_path}:{container_path}"));
}
args.push(image.to_string());
let output = tokio::process::Command::new(&self.runtime)
.args(&args)
.output()
.await?;
if !output.status.success() {
anyhow::bail!("Failed to start container: {}",
String::from_utf8_lossy(&output.stderr));
}
let container_id = String::from_utf8(output.stdout)?.trim().to_string();
info!("Started container {} from image {}", container_id, image);
Ok(container_id)
}
/// Wait for container to be ready
pub async fn wait_for_container_ready(
&self,
container_id: &str,
timeout_secs: u64,
) -> Result<()> {
let timeout_duration = Duration::from_secs(timeout_secs);
let start = Instant::now();
while start.elapsed() < timeout_duration {
let output = tokio::process::Command::new(&self.runtime)
.args(["ps", "-q", "--filter", &format!("id={container_id}")])
.output()
.await?;
if output.status.success() && !output.stdout.is_empty() {
return Ok(());
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
anyhow::bail!("Container {container_id} not ready within {timeout_duration:?}")
}
/// Get container logs
pub async fn get_container_logs(&self, container_id: &str) -> Result<String> {
let output = tokio::process::Command::new(&self.runtime)
.args(["logs", container_id])
.output()
.await?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
/// Stop and remove container
pub async fn cleanup_container(&self, container_id: &str) -> Result<()> {
// Stop container
let _ = tokio::process::Command::new(&self.runtime)
.args(["stop", container_id])
.output()
.await;
// Remove container
let _ = tokio::process::Command::new(&self.runtime)
.args(["rm", container_id])
.output()
.await;
Ok(())
}
}
/// Test data management utilities
pub struct TestDataManager {
pub base_path: std::path::PathBuf,
}
impl TestDataManager {
pub fn new(base_path: std::path::PathBuf) -> Self {
Self { base_path }
}
/// Create a temporary directory for test data
pub async fn create_temp_dir(&self, prefix: &str) -> Result<std::path::PathBuf> {
let temp_dir = tempfile::tempdir_in(&self.base_path)?;
let path = temp_dir.into_path();
// Create subdirectory with prefix
let test_dir = path.join(format!("{}-{}", prefix, Uuid::new_v4()));
std::fs::create_dir_all(&test_dir)?;
Ok(test_dir)
}
/// Save test artifacts for debugging
pub async fn save_artifacts(
&self,
test_name: &str,
artifacts: &[(&str, &[u8])],
) -> Result<std::path::PathBuf> {
let artifacts_dir = self.base_path.join("artifacts").join(test_name);
std::fs::create_dir_all(&artifacts_dir)?;
for (filename, data) in artifacts {
let file_path = artifacts_dir.join(filename);
std::fs::write(&file_path, data)?;
}
Ok(artifacts_dir)
}
/// Load test dataset
pub async fn load_test_dataset(&self, dataset_name: &str) -> Result<Vec<u8>> {
let dataset_path = self.base_path.join("datasets").join(dataset_name);
let data = std::fs::read(dataset_path)?;
Ok(data)
}
}
/// Performance assertion utilities
pub struct PerformanceAssert;
impl PerformanceAssert {
/// Assert that operation completes within time limit
pub async fn assert_duration_max<F, Fut>(
operation: F,
max_duration: Duration,
description: &str,
) -> Result<()>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<()>>,
{
let start = Instant::now();
match timeout(max_duration, operation()).await {
Ok(result) => {
let elapsed = start.elapsed();
info!("{} completed in {:?} (limit: {:?})", description, elapsed, max_duration);
result
}
Err(_) => {
anyhow::bail!("{description} exceeded time limit of {max_duration:?}")
}
}
}
/// Assert that throughput meets minimum requirement
pub fn assert_throughput_min(
operations: usize,
duration: Duration,
min_ops_per_sec: f64,
description: &str,
) -> Result<()> {
let actual_ops_per_sec = operations as f64 / duration.as_secs_f64();
if actual_ops_per_sec >= min_ops_per_sec {
info!("{} throughput: {:.2} ops/sec (required: {:.2})",
description, actual_ops_per_sec, min_ops_per_sec);
Ok(())
} else {
anyhow::bail!("{description} throughput too low: {actual_ops_per_sec:.2} ops/sec (required: {min_ops_per_sec:.2})")
}
}
/// Assert memory usage stays within limits
pub fn assert_memory_max(
current_mb: u64,
max_mb: u64,
description: &str,
) -> Result<()> {
if current_mb <= max_mb {
info!("{} memory usage: {} MB (limit: {} MB)",
description, current_mb, max_mb);
Ok(())
} else {
anyhow::bail!("{description} memory usage too high: {current_mb} MB (limit: {max_mb} MB)")
}
}
}
/// Retry utilities for flaky operations
pub struct RetryUtils;
impl RetryUtils {
/// Retry an async operation with exponential backoff
pub async fn retry_with_backoff<F, Fut, T>(
operation: F,
max_attempts: usize,
initial_delay: Duration,
max_delay: Duration,
description: &str,
) -> Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut attempts = 0;
let mut delay = initial_delay;
loop {
attempts += 1;
match operation().await {
Ok(result) => {
if attempts > 1 {
info!("{} succeeded after {} attempts", description, attempts);
}
return Ok(result);
}
Err(e) if attempts >= max_attempts => {
error!("{} failed after {} attempts: {}", description, attempts, e);
return Err(e);
}
Err(e) => {
warn!("{} attempt {} failed: {}", description, attempts, e);
tokio::time::sleep(delay).await;
delay = std::cmp::min(delay * 2, max_delay);
}
}
}
}
}
/// Test synchronization utilities
pub struct TestSync;
impl TestSync {
/// Wait for multiple async operations to complete
pub async fn wait_all<T>(futures: Vec<impl std::future::Future<Output = Result<T>>>) -> Result<Vec<T>> {
let results = futures::future::try_join_all(futures).await?;
Ok(results)
}
/// Race multiple operations and return the first successful result
pub async fn race<T>(futures: Vec<impl std::future::Future<Output = Result<T>> + Unpin>) -> Result<T> {
let (result, _index, _remaining) = futures::future::select_all(futures).await;
result
}
}