use crate::parser::{Ast, ShellCommand};
use crate::state::{JobStatus, ShellState};
use crate::executor::execute;
use std::thread;
use std::time::Duration;
#[test]
fn test_simple_background_command() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert_eq!(job.job_id, 1);
assert_eq!(job.status, JobStatus::Running);
assert!(!job.is_builtin);
assert_eq!(job.pids.len(), 1);
assert!(job.pgid.is_some());
}
#[test]
fn test_builtin_background_execution() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["pwd".to_string()],
redirections: vec![],
compound: None,
}])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert_eq!(job.job_id, 1);
assert_eq!(job.status, JobStatus::Running);
assert!(job.is_builtin);
assert_eq!(job.pids.len(), 1);
assert!(job.pgid.is_some());
}
#[test]
fn test_pipeline_background_execution() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![
ShellCommand {
args: vec!["echo".to_string(), "hello".to_string()],
redirections: vec![],
compound: None,
},
ShellCommand {
args: vec!["cat".to_string()],
redirections: vec![],
compound: None,
},
])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert_eq!(job.job_id, 1);
assert_eq!(job.status, JobStatus::Running);
assert!(!job.is_builtin);
assert_eq!(job.pids.len(), 2);
assert!(job.pgid.is_some());
}
#[test]
fn test_multiple_background_jobs() {
let mut shell_state = ShellState::new();
let ast1 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast1, &mut shell_state);
let ast2 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast2, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 2);
assert_eq!(jobs[0].job_id, 1);
assert_eq!(jobs[1].job_id, 2);
assert_eq!(jobs[0].status, JobStatus::Running);
assert_eq!(jobs[1].status, JobStatus::Running);
}
#[test]
fn test_job_table_updates() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["true".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert_eq!(job.job_id, 1);
assert_eq!(job.status, JobStatus::Running);
assert_eq!(job_table.get_current_job(), Some(1));
}
#[test]
fn test_process_group_creation() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert!(job.pgid.is_some());
assert_eq!(job.pgid.unwrap(), job.pids[0]);
}
#[test]
fn test_pipeline_process_group() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![
ShellCommand {
args: vec!["echo".to_string(), "test".to_string()],
redirections: vec![],
compound: None,
},
ShellCommand {
args: vec!["cat".to_string()],
redirections: vec![],
compound: None,
},
])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert!(job.pgid.is_some());
assert_eq!(job.pgid.unwrap(), job.pids[0]);
}
#[test]
fn test_background_job_notification() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["true".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
assert_eq!(jobs[0].job_id, 1);
}
#[test]
fn test_empty_background_command() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 0);
}
#[test]
fn test_background_command_with_variable_expansion() {
let mut shell_state = ShellState::new();
shell_state.set_var("DELAY", "0.1".to_string());
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "$DELAY".to_string()],
redirections: vec![],
compound: None,
}])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert!(job.command.contains("$DELAY"));
}
#[test]
fn test_background_builtin_with_output() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["pwd".to_string()],
redirections: vec![],
compound: None,
}])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
assert!(jobs[0].is_builtin);
}
#[test]
fn test_job_id_allocation() {
let mut shell_state = ShellState::new();
for i in 1..=3 {
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["true".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), i);
assert_eq!(jobs[i - 1].job_id, i);
}
}
#[test]
fn test_background_command_stdin_redirection() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["cat".to_string()],
redirections: vec![],
compound: None,
}])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
thread::sleep(Duration::from_millis(50));
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
}
#[test]
fn test_background_job_command_string() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["echo".to_string(), "hello".to_string(), "world".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert!(job.command.contains("echo"));
assert!(job.command.contains("hello"));
assert!(job.command.contains("world"));
}
#[test]
fn test_background_pipeline_command_string() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![
ShellCommand {
args: vec!["echo".to_string(), "test".to_string()],
redirections: vec![],
compound: None,
},
ShellCommand {
args: vec!["grep".to_string(), "test".to_string()],
redirections: vec![],
compound: None,
},
])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert!(job.command.contains("echo"));
assert!(job.command.contains("grep"));
assert!(job.command.contains("|"));
}
#[test]
fn test_integration_start_check_wait_workflow() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
assert_eq!(jobs[0].status, JobStatus::Running);
assert_eq!(jobs[0].job_id, 1);
}
thread::sleep(Duration::from_millis(150));
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
}
#[test]
fn test_integration_multiple_concurrent_jobs() {
let mut shell_state = ShellState::new();
for i in 1..=3 {
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), format!("0.{}", i)],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
}
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 3);
for job in &jobs {
assert_eq!(job.status, JobStatus::Running);
}
assert_eq!(jobs[0].job_id, 1);
assert_eq!(jobs[1].job_id, 2);
assert_eq!(jobs[2].job_id, 3);
assert_eq!(job_table.get_current_job(), Some(3));
assert_eq!(job_table.get_previous_job(), Some(2));
}
thread::sleep(Duration::from_millis(400));
}
#[test]
fn test_integration_pipeline_background_execution() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![
ShellCommand {
args: vec!["echo".to_string(), "test data".to_string()],
redirections: vec![],
compound: None,
},
ShellCommand {
args: vec!["grep".to_string(), "test".to_string()],
redirections: vec![],
compound: None,
},
ShellCommand {
args: vec!["wc".to_string(), "-l".to_string()],
redirections: vec![],
compound: None,
},
])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert_eq!(job.pids.len(), 3); assert!(job.pgid.is_some());
assert_eq!(job.status, JobStatus::Running);
assert_eq!(job.pgid.unwrap(), job.pids[0]);
}
thread::sleep(Duration::from_millis(100));
}
#[test]
fn test_integration_job_control_with_builtins() {
let mut shell_state = ShellState::new();
let builtins = vec!["pwd", ":", "times"];
for (i, builtin) in builtins.iter().enumerate() {
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec![builtin.to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), i + 1);
assert!(jobs[i].is_builtin);
}
thread::sleep(Duration::from_millis(100));
}
#[test]
fn test_integration_job_state_transitions() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["true".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
assert_eq!(jobs[0].status, JobStatus::Running);
}
thread::sleep(Duration::from_millis(50));
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
}
#[test]
fn test_integration_current_previous_job_tracking() {
let mut shell_state = ShellState::new();
let ast1 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast1, &mut shell_state);
{
let job_table = shell_state.job_table.borrow();
assert_eq!(job_table.get_current_job(), Some(1));
assert_eq!(job_table.get_previous_job(), None);
}
let ast2 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast2, &mut shell_state);
{
let job_table = shell_state.job_table.borrow();
assert_eq!(job_table.get_current_job(), Some(2));
assert_eq!(job_table.get_previous_job(), Some(1));
}
let ast3 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast3, &mut shell_state);
{
let job_table = shell_state.job_table.borrow();
assert_eq!(job_table.get_current_job(), Some(3));
assert_eq!(job_table.get_previous_job(), Some(2));
}
}
#[test]
fn test_integration_mixed_builtin_external_jobs() {
let mut shell_state = ShellState::new();
let ast1 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["pwd".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast1, &mut shell_state);
let ast2 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast2, &mut shell_state);
let ast3 = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec![":".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast3, &mut shell_state);
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 3);
assert!(jobs[0].is_builtin);
assert!(!jobs[1].is_builtin);
assert!(jobs[2].is_builtin);
}
}
#[test]
fn test_integration_job_cleanup_on_completion() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["true".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_id = {
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
jobs[0].job_id
};
thread::sleep(Duration::from_millis(50));
{
let mut job_table = shell_state.job_table.borrow_mut();
job_table.remove_job(job_id);
}
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 0);
}
#[test]
fn test_integration_process_group_isolation() {
let mut shell_state = ShellState::new();
for _ in 0..3 {
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["sleep".to_string(), "0.1".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
}
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 3);
let mut pgids = std::collections::HashSet::new();
for job in jobs {
assert!(job.pgid.is_some());
pgids.insert(job.pgid.unwrap());
}
assert_eq!(pgids.len(), 3);
}
}
#[test]
fn test_integration_background_with_variable_expansion() {
let mut shell_state = ShellState::new();
shell_state.set_var("CMD", "sleep".to_string());
shell_state.set_var("ARG", "0.1".to_string());
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["$CMD".to_string(), "$ARG".to_string()],
redirections: vec![],
compound: None,
}])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert!(job.command.contains("$CMD"));
assert!(job.command.contains("$ARG"));
}
}
#[test]
fn test_integration_sequential_job_execution() {
let mut shell_state = ShellState::new();
let commands = vec!["true", "false", "pwd", "echo"];
for (i, cmd) in commands.iter().enumerate() {
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec![cmd.to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), i + 1);
}
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 4);
for (i, job) in jobs.iter().enumerate() {
assert_eq!(job.job_id, i + 1);
}
}
#[test]
fn test_integration_background_pipeline_with_builtins() {
let mut shell_state = ShellState::new();
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![
ShellCommand {
args: vec!["echo".to_string(), "test".to_string()],
redirections: vec![],
compound: None,
},
ShellCommand {
args: vec!["cat".to_string()],
redirections: vec![],
compound: None,
},
])),
};
let exit_code = execute(ast, &mut shell_state);
assert_eq!(exit_code, 0);
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 1);
let job = jobs[0];
assert_eq!(job.pids.len(), 2);
assert!(job.command.contains("echo"));
assert!(job.command.contains("cat"));
assert!(job.command.contains("|"));
}
}
#[test]
fn test_integration_rapid_job_creation() {
let mut shell_state = ShellState::new();
for _ in 0..10 {
let ast = Ast::AsyncCommand {
command: Box::new(Ast::Pipeline(vec![ShellCommand {
args: vec!["true".to_string()],
redirections: vec![],
compound: None,
}])),
};
execute(ast, &mut shell_state);
}
{
let job_table = shell_state.job_table.borrow();
let jobs = job_table.get_all_jobs();
assert_eq!(jobs.len(), 10);
for (i, job) in jobs.iter().enumerate() {
assert_eq!(job.job_id, i + 1);
}
}
}