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
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-06 14:16:26 -03:00
parent fe4c58d155
commit 08fa13b368
3 changed files with 158 additions and 83 deletions

View file

@ -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,

View file

@ -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<Child>,
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<PathBuf> {
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<Self> {
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<Self> {
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<PathBuf> {
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<PathBuf> {
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()));
}

View file

@ -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<Child>,
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<PathBuf> {
// 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<Self> {
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<String> {
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<PathBuf> {
// 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(),