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:
parent
fe4c58d155
commit
08fa13b368
3 changed files with 158 additions and 83 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue