#![cfg(all(feature = "embed", feature = "test-support", feature = "unix-runtime"))]
use mxsh::ShellBuilder;
use mxsh::embed::StdioConfig;
use mxsh::policy::{CommandOverride, VariableAttributes};
use mxsh::runtime::testing::{InMemoryRuntime, StringStdioOut};
#[test]
fn special_host_builtin_keeps_prefix_assignment() {
let mut shell = ShellBuilder::new()
.register_special_builtin("observe", |context, _args| {
let value = context.env_get("X").unwrap_or("").to_string();
let _ = context.env_set("SEEN", value, VariableAttributes::empty());
0
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "X=1 observe");
assert_eq!(result.status, 0);
assert_eq!(shell.env_get("X"), Some("1"));
assert_eq!(shell.env_get("SEEN"), Some("1"));
}
#[test]
fn regular_host_builtin_restores_prefix_assignment() {
let mut shell = ShellBuilder::new()
.register_builtin("observe", |context, _args| {
let value = context.env_get("X").unwrap_or("").to_string();
let _ = context.env_set("SEEN", value, VariableAttributes::empty());
0
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "X=1 observe");
assert_eq!(result.status, 0);
assert_eq!(shell.env_get("X"), None);
assert_eq!(shell.env_get("SEEN"), Some("1"));
}
#[test]
fn command_override_matcher_can_fall_through_to_normal_resolution() {
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.register_command_override(
"echo",
CommandOverride::new(|context, _args| {
let _ = context.write_stdout_line("override");
0
})
.with_matcher(|argv| argv.len() == 1),
)
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "echo; echo hello");
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "override\nhello\n");
}
#[test]
fn command_override_can_update_shell_state() {
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.register_command_override(
"echo",
CommandOverride::new(|context, args| {
let rendered = args.join(" ");
let _ = context.write_stdout_line(&rendered);
let _ = context.env_set("OVERRIDE_OUTPUT", rendered, VariableAttributes::empty());
0
}),
)
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "echo hello world");
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "hello world\n");
assert_eq!(shell.env_get("OVERRIDE_OUTPUT"), Some("hello world"));
}
#[test]
fn command_policy_can_mark_unspecified_utilities() {
let stderr = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.clear_unspecified_utilities()
.add_unspecified_utility("mystery")
.stdio(StdioConfig {
stderr: stderr.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "mystery");
assert_eq!(result.status, 1);
assert_eq!(result.exit_code, Some(1));
assert!(
stderr
.collect()
.contains("mystery: The behavior of this command is undefined.")
);
}
#[test]
fn host_builtin_can_write_using_shell_environment() {
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.env("MSG", "from-shell", VariableAttributes::empty())
.register_builtin("emit-msg", |context, _args| {
let message = context.env_get("MSG").unwrap_or("");
let _ = context.write_stdout_line(message);
0
})
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "emit-msg");
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "from-shell\n");
}
#[test]
fn host_builtin_can_read_shell_identity_and_working_directory() {
let stdout = StringStdioOut::new();
let cwd = std::env::temp_dir().join("mxsh-host-builtin-runtime");
let expected_line = format!("toysh: hello @ {}", cwd.display());
let mut shell = ShellBuilder::new()
.shell_name("toysh")
.current_dir(&cwd)
.env("GREETING", "hello", VariableAttributes::EXPORT)
.register_builtin("invoke", |context, _args| {
let greeting = context.env_get("GREETING").unwrap_or("");
let _ = context.write_stdout_line(&format!(
"{}: {} @ {}",
context.shell_name(),
greeting,
context.current_dir().display()
));
7
})
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "invoke");
assert_eq!(result.status, 7);
assert_eq!(stdout.collect(), format!("{expected_line}\n"));
}
#[test]
fn host_builtin_can_update_working_directory_and_environment() {
let original_dir = std::env::temp_dir().join("mxsh-host-builtin-original");
let updated_dir = std::env::temp_dir().join("mxsh-host-builtin-updated");
let expected_original = original_dir.display().to_string();
let updated_dir_for_builtin = updated_dir.clone();
let mut shell = ShellBuilder::new()
.current_dir(&original_dir)
.register_builtin("rehome", move |context, _args| {
let previous = context.current_dir().display().to_string();
context.set_current_dir(updated_dir_for_builtin.clone());
let _ = context.env_set("PREVIOUS_CWD", previous, VariableAttributes::empty());
0
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let result = shell.run(&mut runtime, "rehome");
assert_eq!(result.status, 0);
assert_eq!(
shell.env_get("PREVIOUS_CWD"),
Some(expected_original.as_str())
);
assert_eq!(shell.current_dir(), updated_dir.as_path());
}
#[test]
fn configs_can_install_distinct_builtin_registries() {
let mut empty = ShellBuilder::new()
.clear_builtins()
.new_session()
.expect("session should build");
let custom_stdout = StringStdioOut::new();
let mut custom = ShellBuilder::new()
.clear_builtins()
.register_builtin("hello", |context, args| {
let _ = context.write_stdout_line(&args.join(" "));
0
})
.stdio(StdioConfig {
stdout: custom_stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let standard_stdout = StringStdioOut::new();
let mut standard = ShellBuilder::new()
.stdio(StdioConfig {
stdout: standard_stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
assert_eq!(empty.run(&mut runtime, "echo missing").status, 127);
assert_eq!(custom.run(&mut runtime, "hello custom builtin").status, 0);
assert_eq!(
standard.run(&mut runtime, "echo standard builtin").status,
0
);
assert_eq!(custom_stdout.collect(), "custom builtin\n");
assert_eq!(standard_stdout.collect(), "standard builtin\n");
}
#[test]
fn configs_can_reclassify_builtin_specialness() {
let mut regular = ShellBuilder::new()
.clear_builtins()
.register_builtin("echo", |context, _args| {
let value = context.env_get("X").unwrap_or("").to_string();
let _ = context.env_set("SEEN", value, VariableAttributes::empty());
0
})
.new_session()
.expect("session should build");
let mut special = ShellBuilder::new()
.clear_builtins()
.register_special_builtin("echo", |context, _args| {
let value = context.env_get("X").unwrap_or("").to_string();
let _ = context.env_set("SEEN", value, VariableAttributes::empty());
0
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
assert_eq!(regular.run(&mut runtime, "X=1 echo").status, 0);
assert_eq!(special.run(&mut runtime, "X=1 echo").status, 0);
assert_eq!(regular.env_get("X"), None);
assert_eq!(special.env_get("X"), Some("1"));
assert_eq!(regular.env_get("SEEN"), Some("1"));
assert_eq!(special.env_get("SEEN"), Some("1"));
}