use std::fs;
use gshell::{
parser::Parser,
runtime::{BootstrapExecutor, Executor},
shell::{ExitCode, ShellAction, ShellState},
};
#[tokio::test]
async fn output_truncate_redirection_works() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let out = dir.path().join("out.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!("echo hello > {}", out.display()))
.expect("parse should succeed");
let result = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
match result {
ShellAction::Continue(output) => {
assert_eq!(output.exit_code, ExitCode::SUCCESS);
assert!(output.stdout.is_empty());
}
ShellAction::Exit(_) => panic!("echo should not exit"),
}
let content = fs::read_to_string(out).expect("output file should be readable");
assert_eq!(content, "hello\n");
}
#[tokio::test]
async fn output_append_redirection_works() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let out = dir.path().join("out.txt");
fs::write(&out, "first\n").expect("seed output file should be writable");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!("echo second >> {}", out.display()))
.expect("parse should succeed");
let _ = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
let content = fs::read_to_string(out).expect("output file should be readable");
assert_eq!(content, "first\nsecond\n");
}
#[tokio::test]
async fn output_append_redirection_uses_shell_working_directory() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let nested = dir.path().join("nested");
fs::create_dir(&nested).expect("nested dir should be created");
let out = nested.join("out.txt");
fs::write(&out, "first\n").expect("seed output file should be writable");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
state.write().await.set_cwd(nested.clone());
let parsed = parser
.parse("echo second >> out.txt")
.expect("parse should succeed");
let _ = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
let content = fs::read_to_string(&out).expect("output file should be readable");
assert_eq!(content, "first\nsecond\n");
}
#[tokio::test]
async fn stderr_truncate_redirection_works() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let err = dir.path().join("err.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!("type nope 2> {}", err.display()))
.expect("parse should succeed");
let result = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
match result {
ShellAction::Continue(output) => {
assert!(output.stderr.is_empty());
assert_eq!(output.exit_code, ExitCode::FAILURE);
}
ShellAction::Exit(_) => panic!("type should not exit"),
}
let content = fs::read_to_string(err).expect("stderr file should be readable");
assert!(content.contains("not found"));
}
#[tokio::test]
async fn stderr_append_redirection_works() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let err = dir.path().join("err.txt");
fs::write(&err, "before\n").expect("seed stderr file should be writable");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!("type nope 2>> {}", err.display()))
.expect("parse should succeed");
let _ = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
let content = fs::read_to_string(err).expect("stderr file should be readable");
assert!(content.starts_with("before\n"));
assert!(content.contains("not found"));
}
#[tokio::test]
async fn redirected_builtin_does_not_write_to_terminal_output_buffer() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let out = dir.path().join("out.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!("pwd > {}", out.display()))
.expect("parse should succeed");
let result = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
match result {
ShellAction::Continue(output) => {
assert!(output.stdout.is_empty());
assert_eq!(output.exit_code, ExitCode::SUCCESS);
}
ShellAction::Exit(_) => panic!("pwd should not exit"),
}
let content = fs::read_to_string(out).expect("output file should be readable");
assert!(!content.trim().is_empty());
}
#[tokio::test]
async fn output_redirection_target_expands_environment_variables() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let out = dir.path().join("expanded-out.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
state
.write()
.await
.set_env_var("OUTFILE", out.display().to_string());
let parsed = parser
.parse("echo hello > \"$OUTFILE\"")
.expect("parse should succeed");
let result = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
match result {
ShellAction::Continue(output) => {
assert_eq!(output.exit_code, ExitCode::SUCCESS);
assert!(output.stdout.is_empty());
}
ShellAction::Exit(_) => panic!("echo should not exit"),
}
let content = fs::read_to_string(out).expect("output file should be readable");
assert_eq!(content, "hello\n");
}
#[tokio::test]
async fn input_redirection_target_expands_environment_variables() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let input = dir.path().join("expanded-in.txt");
let output = dir.path().join("captured-out.txt");
fs::write(&input, "hello from file\n").expect("input file should be writable");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
state
.write()
.await
.set_env_var("INFILE", input.display().to_string());
state
.write()
.await
.set_env_var("OUTFILE", output.display().to_string());
let parsed = parser
.parse("cat < \"$INFILE\" > \"$OUTFILE\"")
.expect("parse should succeed");
let result = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
match result {
ShellAction::Continue(output) => {
assert_eq!(output.exit_code, ExitCode::SUCCESS);
assert!(output.stdout.is_empty());
}
ShellAction::Exit(_) => panic!("cat should not exit"),
}
let content = fs::read_to_string(output).expect("captured output should be readable");
assert_eq!(content, "hello from file\n");
}
#[tokio::test]
async fn basic_heredoc_execution_feeds_stdin() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let output = dir.path().join("captured.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!(
"cat <<EOF > {}\nhello from heredoc\nEOF\n",
output.display()
))
.expect("parse should succeed");
let result = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
match result {
ShellAction::Continue(output) => {
assert_eq!(output.exit_code, ExitCode::SUCCESS);
assert!(output.stdout.is_empty());
}
ShellAction::Exit(_) => panic!("cat should not exit"),
}
let content = fs::read_to_string(output).expect("captured output should be readable");
assert_eq!(content, "hello from heredoc\n");
}
#[tokio::test]
async fn unquoted_heredoc_expands_environment_variables() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let output = dir.path().join("captured.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
state.write().await.set_env_var("HEREDOC_VALUE", "expanded");
let parsed = parser
.parse(&format!(
"cat <<EOF > {}\nvalue:$HEREDOC_VALUE\nEOF\n",
output.display()
))
.expect("parse should succeed");
let _ = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
let content = fs::read_to_string(output).expect("captured output should be readable");
assert_eq!(content, "value:expanded\n");
}
#[tokio::test]
async fn quoted_heredoc_preserves_literal_body_text() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let output = dir.path().join("captured.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
state.write().await.set_env_var("HEREDOC_VALUE", "expanded");
let parsed = parser
.parse(&format!(
"cat <<'EOF' > {}\nvalue:$HEREDOC_VALUE\nEOF\n",
output.display()
))
.expect("parse should succeed");
let _ = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
let content = fs::read_to_string(output).expect("captured output should be readable");
assert_eq!(content, "value:$HEREDOC_VALUE\n");
}
#[tokio::test]
async fn last_heredoc_wins_when_multiple_are_present() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let output = dir.path().join("captured.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!(
"cat <<FIRST <<SECOND > {}\nfirst body\nFIRST\nsecond body\nSECOND\n",
output.display()
))
.expect("parse should succeed");
let _ = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
let content = fs::read_to_string(output).expect("captured output should be readable");
assert_eq!(content, "second body\n");
}
#[tokio::test]
async fn unquoted_heredoc_runs_command_substitution() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let output = dir.path().join("captured.txt");
let parser = Parser::default();
let executor = BootstrapExecutor;
let state = ShellState::shared().await.expect("state should initialize");
let parsed = parser
.parse(&format!(
"cat <<EOF > {}\n$(printf 'hello')\nEOF\n",
output.display()
))
.expect("parse should succeed");
let _ = executor
.execute(state, &parsed)
.await
.expect("execution should succeed");
let content = fs::read_to_string(output).expect("captured output should be readable");
assert_eq!(content, "hello\n");
}