From 08fa13b368ab752420a0a1709c6f7626dffdc92a Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sat, 6 Dec 2025 14:16:26 -0300 Subject: [PATCH] Fix test harness to use botserver-stack binaries - PostgresService now uses binaries from botserver-stack/bin/tables/bin - MinioService now uses binary from botserver-stack/bin/drive/minio - Use absolute paths for PostgreSQL unix_socket_directories - Add PostgreSQL startup log file for debugging - Disable MinIO and Redis in full() config (MinIO segfaults, Redis not in stack) - 38/41 e2e tests now pass --- src/harness.rs | 4 +- src/services/minio.rs | 78 +++++++++++-------- src/services/postgres.rs | 159 ++++++++++++++++++++++++++------------- 3 files changed, 158 insertions(+), 83 deletions(-) diff --git a/src/harness.rs b/src/harness.rs index b636b32..3421b58 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -49,8 +49,8 @@ impl TestConfig { pub fn full() -> Self { Self { postgres: true, - minio: true, - redis: true, + minio: false, // MinIO binary in botserver-stack is broken (segfault) + redis: false, // Redis not in botserver-stack mock_zitadel: true, mock_llm: true, run_migrations: true, diff --git a/src/services/minio.rs b/src/services/minio.rs index 28fa894..4420515 100644 --- a/src/services/minio.rs +++ b/src/services/minio.rs @@ -1,6 +1,6 @@ //! MinIO service management for test infrastructure //! -//! Starts and manages a MinIO instance for S3-compatible storage testing. +//! Uses the MinIO binary from botserver-stack folder. //! Provides bucket creation, object operations, and credential management. use super::{check_tcp_port, ensure_dir, wait_for, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT}; @@ -18,6 +18,7 @@ pub struct MinioService { api_port: u16, console_port: u16, data_dir: PathBuf, + bin_path: PathBuf, process: Option, access_key: String, secret_key: String, @@ -30,8 +31,35 @@ impl MinioService { /// Default secret key for tests pub const DEFAULT_SECRET_KEY: &'static str = "minioadmin"; + /// Find the botserver-stack path and return minio binary + fn find_minio_binary() -> Result { + let cwd = std::env::current_dir()?; + + let candidates = [ + cwd.join("../botserver/botserver-stack/bin/drive/minio"), + cwd.join("botserver/botserver-stack/bin/drive/minio"), + PathBuf::from("/home/rodriguez/src/gb/botserver/botserver-stack/bin/drive/minio"), + std::env::var("BOTSERVER_STACK_PATH") + .map(|p| PathBuf::from(p).join("bin/drive/minio")) + .unwrap_or_default(), + ]; + + for candidate in &candidates { + if candidate.exists() { + return Ok(candidate.clone()); + } + } + + // Fallback to system minio + which::which("minio") + .context("minio not found in botserver-stack or PATH. Set BOTSERVER_STACK_PATH env var") + } + /// Start a new MinIO instance on the specified port pub async fn start(api_port: u16, data_dir: &str) -> Result { + let bin_path = Self::find_minio_binary()?; + log::info!("Using MinIO from: {:?}", bin_path); + let data_path = PathBuf::from(data_dir).join("minio"); ensure_dir(&data_path)?; @@ -42,6 +70,7 @@ impl MinioService { api_port, console_port, data_dir: data_path, + bin_path, process: None, access_key: Self::DEFAULT_ACCESS_KEY.to_string(), secret_key: Self::DEFAULT_SECRET_KEY.to_string(), @@ -60,6 +89,9 @@ impl MinioService { access_key: &str, secret_key: &str, ) -> Result { + let bin_path = Self::find_minio_binary()?; + log::info!("Using MinIO from: {:?}", bin_path); + let data_path = PathBuf::from(data_dir).join("minio"); ensure_dir(&data_path)?; @@ -69,6 +101,7 @@ impl MinioService { api_port, console_port, data_dir: data_path, + bin_path, process: None, access_key: access_key.to_string(), secret_key: secret_key.to_string(), @@ -88,9 +121,7 @@ impl MinioService { self.console_port ); - let minio = Self::find_binary()?; - - let child = Command::new(&minio) + let child = Command::new(&self.bin_path) .args([ "server", self.data_dir.to_str().unwrap(), @@ -155,7 +186,11 @@ impl MinioService { .output(); let output = Command::new(&mc) - .args(["mb", "--ignore-existing", &format!("{}/{}", alias_name, name)]) + .args([ + "mb", + "--ignore-existing", + &format!("{}/{}", alias_name, name), + ]) .output()?; if !output.status.success() { @@ -347,32 +382,9 @@ impl MinioService { config } - /// Find the MinIO binary - fn find_binary() -> Result { - let common_paths = [ - "/usr/local/bin/minio", - "/usr/bin/minio", - "/opt/minio/minio", - "/opt/homebrew/bin/minio", - ]; - - for path in common_paths { - let p = PathBuf::from(path); - if p.exists() { - return Ok(p); - } - } - - which::which("minio").context("minio binary not found in PATH or common locations") - } - /// Find the MinIO client (mc) binary fn find_mc_binary() -> Result { - let common_paths = [ - "/usr/local/bin/mc", - "/usr/bin/mc", - "/opt/homebrew/bin/mc", - ]; + let common_paths = ["/usr/local/bin/mc", "/usr/bin/mc", "/opt/homebrew/bin/mc"]; for path in common_paths { let p = PathBuf::from(path); @@ -444,6 +456,7 @@ mod tests { api_port: 9000, console_port: 10000, data_dir: PathBuf::from("/tmp/test"), + bin_path: PathBuf::from("/tmp/minio"), process: None, access_key: "test".to_string(), secret_key: "secret".to_string(), @@ -459,6 +472,7 @@ mod tests { api_port: 9000, console_port: 10000, data_dir: PathBuf::from("/tmp/test"), + bin_path: PathBuf::from("/tmp/minio"), process: None, access_key: "mykey".to_string(), secret_key: "mysecret".to_string(), @@ -475,13 +489,17 @@ mod tests { api_port: 9000, console_port: 10000, data_dir: PathBuf::from("/tmp/test"), + bin_path: PathBuf::from("/tmp/minio"), process: None, access_key: "access".to_string(), secret_key: "secret".to_string(), }; let config = service.s3_config(); - assert_eq!(config.get("endpoint_url"), Some(&"http://127.0.0.1:9000".to_string())); + assert_eq!( + config.get("endpoint_url"), + Some(&"http://127.0.0.1:9000".to_string()) + ); assert_eq!(config.get("access_key_id"), Some(&"access".to_string())); assert_eq!(config.get("force_path_style"), Some(&"true".to_string())); } diff --git a/src/services/postgres.rs b/src/services/postgres.rs index 6095dbf..d6ee15c 100644 --- a/src/services/postgres.rs +++ b/src/services/postgres.rs @@ -1,7 +1,6 @@ //! PostgreSQL service management for test infrastructure //! -//! Starts and manages a PostgreSQL instance for integration testing. -//! Uses the system PostgreSQL installation or botserver's embedded database. +//! Uses the PostgreSQL binaries from botserver-stack folder. use super::{check_tcp_port, ensure_dir, wait_for, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_TIMEOUT}; use anyhow::{Context, Result}; @@ -16,6 +15,7 @@ use tokio::time::sleep; pub struct PostgresService { port: u16, data_dir: PathBuf, + bin_dir: PathBuf, process: Option, connection_string: String, database_name: String, @@ -33,14 +33,51 @@ impl PostgresService { /// Default password for tests pub const DEFAULT_PASSWORD: &'static str = "bottest"; + /// Find the botserver-stack path + fn find_stack_path() -> Result { + // Try relative to current working directory + let cwd = std::env::current_dir()?; + + // Look for botserver-stack in various locations + let candidates = [ + // From bottest directory + cwd.join("../botserver/botserver-stack"), + // From gb root + cwd.join("botserver/botserver-stack"), + // Absolute path + PathBuf::from("/home/rodriguez/src/gb/botserver/botserver-stack"), + // From BOTSERVER_STACK_PATH env var + std::env::var("BOTSERVER_STACK_PATH") + .map(PathBuf::from) + .unwrap_or_default(), + ]; + + for candidate in &candidates { + let bin_path = candidate.join("bin/tables/bin"); + if bin_path.exists() && bin_path.join("postgres").exists() { + return Ok(candidate.clone()); + } + } + + anyhow::bail!( + "botserver-stack not found. Set BOTSERVER_STACK_PATH env var or ensure botserver-stack exists" + ) + } + /// Start a new PostgreSQL instance on the specified port pub async fn start(port: u16, data_dir: &str) -> Result { + let stack_path = Self::find_stack_path()?; + let bin_dir = stack_path.join("bin/tables/bin"); + + log::info!("Using PostgreSQL from botserver-stack: {:?}", bin_dir); + let data_path = PathBuf::from(data_dir).join("postgres"); ensure_dir(&data_path)?; let mut service = Self { port, data_dir: data_path.clone(), + bin_dir, process: None, connection_string: String::new(), database_name: Self::DEFAULT_DATABASE.to_string(), @@ -67,6 +104,11 @@ impl PostgresService { Ok(service) } + /// Get binary path from bin_dir + fn get_binary(&self, name: &str) -> PathBuf { + self.bin_dir.join(name) + } + /// Initialize the database cluster async fn init_db(&self) -> Result<()> { log::info!( @@ -74,9 +116,13 @@ impl PostgresService { self.data_dir ); - let initdb = Self::find_binary("initdb")?; + let initdb = self.get_binary("initdb"); let output = Command::new(&initdb) + .env( + "LD_LIBRARY_PATH", + self.bin_dir.parent().unwrap().join("lib"), + ) .args([ "-D", self.data_dir.to_str().unwrap(), @@ -89,7 +135,7 @@ impl PostgresService { "--no-locale", ]) .output() - .context("Failed to run initdb")?; + .context(format!("Failed to run initdb from {:?}", initdb))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -105,6 +151,11 @@ impl PostgresService { /// Configure PostgreSQL for fast testing (reduced durability) fn configure_for_testing(&self) -> Result<()> { let config_path = self.data_dir.join("postgresql.conf"); + // Use absolute path for unix_socket_directories + let abs_data_dir = self + .data_dir + .canonicalize() + .unwrap_or_else(|_| self.data_dir.clone()); let config = format!( r#" # Test configuration - optimized for speed, not durability @@ -126,7 +177,7 @@ log_duration = off unix_socket_directories = '{}' "#, self.port, - self.data_dir.to_str().unwrap() + abs_data_dir.to_str().unwrap() ); std::fs::write(&config_path, config)?; @@ -137,14 +188,24 @@ unix_socket_directories = '{}' async fn start_server(&mut self) -> Result<()> { log::info!("Starting PostgreSQL on port {}", self.port); - let postgres = Self::find_binary("postgres")?; + let postgres = self.get_binary("postgres"); + let lib_dir = self.bin_dir.parent().unwrap().join("lib"); + + // Create log file for debugging + let log_path = self.data_dir.join("postgres.log"); + let log_file = std::fs::File::create(&log_path) + .context(format!("Failed to create log file {:?}", log_path))?; + let stderr_file = log_file.try_clone()?; + + log::info!("PostgreSQL log file: {:?}", log_path); let child = Command::new(&postgres) + .env("LD_LIBRARY_PATH", &lib_dir) .args(["-D", self.data_dir.to_str().unwrap()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stdout(Stdio::from(log_file)) + .stderr(Stdio::from(stderr_file)) .spawn() - .context("Failed to start PostgreSQL")?; + .context(format!("Failed to start PostgreSQL from {:?}", postgres))?; self.process = Some(child); Ok(()) @@ -154,25 +215,36 @@ unix_socket_directories = '{}' async fn wait_ready(&self) -> Result<()> { log::info!("Waiting for PostgreSQL to be ready..."); - wait_for(HEALTH_CHECK_TIMEOUT, HEALTH_CHECK_INTERVAL, || async { + let result = wait_for(HEALTH_CHECK_TIMEOUT, HEALTH_CHECK_INTERVAL, || async { check_tcp_port("127.0.0.1", self.port).await }) - .await - .context("PostgreSQL failed to start in time")?; + .await; + + if result.is_err() { + // Read log file to show error + let log_path = self.data_dir.join("postgres.log"); + if log_path.exists() { + if let Ok(log_content) = std::fs::read_to_string(&log_path) { + log::error!("PostgreSQL log:\n{}", log_content); + } + } + return Err(result.unwrap_err()).context("PostgreSQL failed to start in time"); + } // Additional wait for pg_isready - let pg_isready = Self::find_binary("pg_isready").ok(); - if let Some(pg_isready) = pg_isready { - for _ in 0..30 { - let status = Command::new(&pg_isready) - .args(["-h", "127.0.0.1", "-p", &self.port.to_string()]) - .status(); + let pg_isready = self.get_binary("pg_isready"); + let lib_dir = self.bin_dir.parent().unwrap().join("lib"); - if status.map(|s| s.success()).unwrap_or(false) { - return Ok(()); - } - sleep(Duration::from_millis(100)).await; + for _ in 0..30 { + let status = Command::new(&pg_isready) + .env("LD_LIBRARY_PATH", &lib_dir) + .args(["-h", "127.0.0.1", "-p", &self.port.to_string()]) + .status(); + + if status.map(|s| s.success()).unwrap_or(false) { + return Ok(()); } + sleep(Duration::from_millis(100)).await; } Ok(()) @@ -182,10 +254,12 @@ unix_socket_directories = '{}' async fn setup_test_database(&self) -> Result<()> { log::info!("Setting up test database '{}'", self.database_name); - let psql = Self::find_binary("psql")?; + let psql = self.get_binary("psql"); + let lib_dir = self.bin_dir.parent().unwrap().join("lib"); // Create user let _ = Command::new(&psql) + .env("LD_LIBRARY_PATH", &lib_dir) .args([ "-h", "127.0.0.1", @@ -203,6 +277,7 @@ unix_socket_directories = '{}' // Create database let _ = Command::new(&psql) + .env("LD_LIBRARY_PATH", &lib_dir) .args([ "-h", "127.0.0.1", @@ -248,9 +323,11 @@ unix_socket_directories = '{}' /// Create a new database with the given name pub async fn create_database(&self, name: &str) -> Result<()> { - let psql = Self::find_binary("psql")?; + let psql = self.get_binary("psql"); + let lib_dir = self.bin_dir.parent().unwrap().join("lib"); let output = Command::new(&psql) + .env("LD_LIBRARY_PATH", &lib_dir) .args([ "-h", "127.0.0.1", @@ -275,9 +352,11 @@ unix_socket_directories = '{}' /// Execute raw SQL pub async fn execute(&self, sql: &str) -> Result<()> { - let psql = Self::find_binary("psql")?; + let psql = self.get_binary("psql"); + let lib_dir = self.bin_dir.parent().unwrap().join("lib"); let output = Command::new(&psql) + .env("LD_LIBRARY_PATH", &lib_dir) .args([ "-h", "127.0.0.1", @@ -302,9 +381,11 @@ unix_socket_directories = '{}' /// Execute SQL and return results as JSON pub async fn query(&self, sql: &str) -> Result { - let psql = Self::find_binary("psql")?; + let psql = self.get_binary("psql"); + let lib_dir = self.bin_dir.parent().unwrap().join("lib"); let output = Command::new(&psql) + .env("LD_LIBRARY_PATH", &lib_dir) .args([ "-h", "127.0.0.1", @@ -347,31 +428,6 @@ unix_socket_directories = '{}' ) } - /// Find a PostgreSQL binary - fn find_binary(name: &str) -> Result { - // Try common locations - let common_paths = [ - format!("/usr/bin/{}", name), - format!("/usr/local/bin/{}", name), - format!("/usr/lib/postgresql/16/bin/{}", name), - format!("/usr/lib/postgresql/15/bin/{}", name), - format!("/usr/lib/postgresql/14/bin/{}", name), - format!("/opt/homebrew/bin/{}", name), - format!("/opt/homebrew/opt/postgresql@16/bin/{}", name), - format!("/opt/homebrew/opt/postgresql@15/bin/{}", name), - ]; - - for path in common_paths { - let p = PathBuf::from(&path); - if p.exists() { - return Ok(p); - } - } - - // Try which - which::which(name).context(format!("{} not found in PATH or common locations", name)) - } - /// Stop the PostgreSQL server pub async fn stop(&mut self) -> Result<()> { if let Some(ref mut child) = self.process { @@ -436,6 +492,7 @@ mod tests { let service = PostgresService { port: 5432, data_dir: PathBuf::from("/tmp/test"), + bin_dir: PathBuf::from("/tmp/bin"), process: None, connection_string: String::new(), database_name: "testdb".to_string(),