271 lines
7.9 KiB
Rust
271 lines
7.9 KiB
Rust
//! Integration tests for workshop-engine
|
|
//!
|
|
//! These tests execute real commands and verify results.
|
|
//! Run with: cargo test --test engine_integration -p workshop-engine
|
|
//!
|
|
//! Note: These are TRUE integration tests - they run in a separate test binary
|
|
//! and execute actual commands on the system.
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
use workshop_engine::{Engine, ExecutionContext};
|
|
|
|
fn create_executor() -> Engine {
|
|
Engine::new()
|
|
}
|
|
|
|
fn create_context() -> ExecutionContext {
|
|
ExecutionContext {
|
|
task_id: "test-integration-1".to_string(),
|
|
pipeline_name: "test-pipeline".to_string(),
|
|
username: "testuser".to_string(),
|
|
working_dir: std::env::temp_dir(),
|
|
env_vars: HashMap::new(),
|
|
timeout: Some(Duration::from_secs(30)),
|
|
shell: "bash".to_string(),
|
|
dry_run: false,
|
|
privileged: false,
|
|
standalone: false,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_echo_outputs_correct_message() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let mut cmd = Command::new("echo");
|
|
cmd.arg("hello world");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert_eq!(result.exit_code, 0);
|
|
assert!(result.stdout.contains("hello world"));
|
|
assert!(result.stderr.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_true_returns_zero_exit_code() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("true");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert_eq!(result.exit_code, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_false_returns_nonzero_exit_code() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("false");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await;
|
|
|
|
assert!(result.is_err());
|
|
// assert!(!result.success);
|
|
// assert_ne!(result.exit_code, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_ls_lists_directory() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let mut cmd = Command::new("ls");
|
|
cmd.arg("-la");
|
|
cmd.arg("/tmp");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.success);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_date_command() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("date");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(!result.stdout.trim().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_invalid_command_returns_error() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("this_command_definitely_does_not_exist_12345");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await;
|
|
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_passes_custom_env_var() {
|
|
let executor = create_executor();
|
|
let mut ctx = create_context();
|
|
ctx.env_vars
|
|
.insert("MY_CUSTOM_VAR".to_string(), "my_value_123".to_string());
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("env");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.stdout.contains("MY_CUSTOM_VAR=my_value_123"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_sets_ws_taskid() {
|
|
let executor = create_executor();
|
|
let mut ctx = create_context();
|
|
ctx.task_id = "task-abc-123".to_string();
|
|
ctx.pipeline_name = "pipeline-xyz".to_string();
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("env");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.stdout.contains("HBW_TASKID=task-abc-123"));
|
|
assert!(result.stdout.contains("HBW_PIPELINE=pipeline-xyz"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_sets_ws_user_info() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("env");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.stdout.contains("HBW_USERNAME=testuser"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_respects_working_directory() {
|
|
let executor = create_executor();
|
|
let ctx = ExecutionContext {
|
|
task_id: "test-context-1".to_string(),
|
|
pipeline_name: "test-pipeline".to_string(),
|
|
username: "testuser".to_string(),
|
|
working_dir: PathBuf::from("/tmp"),
|
|
env_vars: HashMap::new(),
|
|
timeout: Some(Duration::from_secs(30)),
|
|
shell: "bash".to_string(),
|
|
standalone: false,
|
|
..Default::default()
|
|
};
|
|
|
|
use tokio::process::Command;
|
|
let cmd = Command::new("pwd");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.success);
|
|
let trimmed = result.stdout.trim();
|
|
assert!(trimmed == "/tmp" || trimmed.ends_with("tmp"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_execute_captures_stderr() {
|
|
let executor = create_executor();
|
|
let ctx = create_context();
|
|
|
|
use tokio::process::Command;
|
|
let mut cmd = Command::new("bash");
|
|
cmd.arg("-c");
|
|
cmd.arg("echo error >&2");
|
|
|
|
let result = executor.execute(cmd, &ctx, &None).await.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(result.stderr.contains("error"));
|
|
}
|
|
|
|
/// Test: with `process_group(0)`, kill() propagates to the entire process tree.
|
|
///
|
|
/// We spawn a bash that creates a background sleep. With process_group(0),
|
|
/// bash is the process group leader. When we kill(-PGID), the sleep should die too.
|
|
///
|
|
/// This test uses tokio::process::Command + process_group(0) directly
|
|
/// (bypassing Engine) to verify the kernel-level behavior.
|
|
#[tokio::test]
|
|
async fn test_process_group_kill_kills_subprocesses() {
|
|
use std::os::unix::process::CommandExt;
|
|
use std::process::Stdio;
|
|
use tokio::io::AsyncBufReadExt;
|
|
use tokio::process::Command;
|
|
|
|
let mut cmd = Command::new("bash");
|
|
cmd.args(["-c", "sleep 999 & echo $!"]);
|
|
cmd.process_group(0);
|
|
cmd.stdin(Stdio::null());
|
|
cmd.stdout(Stdio::piped());
|
|
cmd.stderr(Stdio::piped());
|
|
|
|
let mut child = cmd.spawn().expect("spawn failed");
|
|
let bash_pid = child.id().expect("no pid");
|
|
eprintln!("[test] bash PID: {}", bash_pid);
|
|
|
|
// Read the background sleep's PID from stdout
|
|
let stdout = child.stdout.take().expect("stdout pipe");
|
|
let mut reader = tokio::io::BufReader::new(stdout);
|
|
let mut pid_line = String::new();
|
|
reader
|
|
.read_line(&mut pid_line)
|
|
.await
|
|
.expect("read pid line");
|
|
let sleep_pid: u32 = pid_line.trim().parse().expect("valid sleep PID");
|
|
eprintln!("[test] sleep PID: {}", sleep_pid);
|
|
|
|
// Verify sleep is alive before kill
|
|
let alive_before = std::path::Path::new(&format!("/proc/{}", sleep_pid)).exists();
|
|
eprintln!("[test] sleep alive before kill: {}", alive_before);
|
|
|
|
// Kill by sending SIGKILL to the NEGATIVE PGID (entire process group)
|
|
unsafe {
|
|
libc::kill(-(bash_pid as i32), libc::SIGKILL);
|
|
}
|
|
child.wait().await.expect("wait failed");
|
|
eprintln!("[test] kill+wait done");
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
|
|
|
// Check if sleep survived — it SHOULD be dead now
|
|
let alive_after = std::path::Path::new(&format!("/proc/{}", sleep_pid)).exists();
|
|
eprintln!("[test] sleep alive after kill: {}", alive_after);
|
|
|
|
// Cleanup just in case
|
|
let _ = std::process::Command::new("kill")
|
|
.arg("-9")
|
|
.arg(sleep_pid.to_string())
|
|
.spawn();
|
|
|
|
assert!(alive_before, "Test setup: sleep was never created");
|
|
assert!(
|
|
!alive_after,
|
|
"sleep survived process-group kill! process_group(0) fix did not work."
|
|
);
|
|
}
|