#![cfg(all(feature = "embed", feature = "test-support"))]
use mxsh::ShellBuilder;
use mxsh::advanced::{
DeferredExpansion, DeferredPipelineStageWork, DeferredReason, PlannedAndOr,
PlannedPipelineStage, PlannedSimpleCommandKind, Planner,
};
use mxsh::ast::{AndOrList, BinOpType, Command as AstCommand, Pipeline, Program};
use mxsh::runtime::testing::InMemoryRuntime;
fn first_command(program: &Program) -> &AstCommand {
let AndOrList::Pipeline(pipeline) = program.body()[0].and_or_list() else {
panic!("expected a pipeline");
};
&pipeline.commands()[0]
}
fn first_pipeline(program: &Program) -> &Pipeline {
let AndOrList::Pipeline(pipeline) = program.body()[0].and_or_list() else {
panic!("expected a pipeline");
};
pipeline
}
#[test]
fn plan_simple_command_exposes_builtin_phase_information() {
let program = Program::parse("echo hello\n").expect("script should parse");
let AstCommand::Simple(command) = first_command(&program) else {
panic!("expected a simple command");
};
let mut shell = ShellBuilder::new()
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let plan = Planner::new(&mut shell, &mut runtime)
.prepare_simple_command(command)
.expect("planning should succeed");
assert_eq!(plan.source_line(), Some(1));
assert!(plan.assignments().is_empty());
assert!(plan.redirects().is_empty());
match plan.kind() {
PlannedSimpleCommandKind::Builtin { argv } => {
assert_eq!(argv, &["echo".to_string(), "hello".to_string()]);
}
other => panic!("expected builtin plan, got {other:?}"),
}
}
#[test]
fn plan_program_preserves_deferred_execution_boundaries() {
let program = Program::parse("echo left && echo right\n").expect("script should parse");
let mut shell = ShellBuilder::new()
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let plan = Planner::new(&mut shell, &mut runtime).prepare(&program);
match plan.command_lists()[0].and_or_list() {
PlannedAndOr::BinOp { op, left, right } => {
assert_eq!(*op, BinOpType::And);
assert_eq!(right.reason(), DeferredReason::ShortCircuitRhs);
match left.as_ref() {
PlannedAndOr::Pipeline(pipeline) => {
assert!(matches!(
pipeline.stages(),
[PlannedPipelineStage::Lazy(lazy)]
if lazy.reason() == DeferredReason::NeedsCurrentShellState
));
}
other => panic!("expected left pipeline, got {other:?}"),
}
}
other => panic!("expected binop plan, got {other:?}"),
}
}
#[test]
fn plan_pipeline_classifies_external_and_shell_stages() {
let program = Program::parse("emit hi | : | upper\n").expect("script should parse");
let pipeline = first_pipeline(&program);
let mut shell = ShellBuilder::new()
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
runtime.register_command("emit", |_argv, _env, _cwd, _stdio| 0);
runtime.register_command("upper", |_argv, _env, _cwd, _stdio| 0);
let plan = Planner::new(&mut shell, &mut runtime).prepare_pipeline(pipeline);
assert!(matches!(
plan.stages(),
[
PlannedPipelineStage::PreparedExternal(_),
PlannedPipelineStage::SimpleCommand(_),
PlannedPipelineStage::PreparedExternal(_)
]
));
match &plan.stages()[1] {
PlannedPipelineStage::SimpleCommand(plan) => match plan.kind() {
PlannedSimpleCommandKind::Builtin { argv } => {
assert_eq!(argv, &[":".to_string()]);
}
other => panic!("expected builtin middle stage, got {other:?}"),
},
other => panic!("expected shell stage, got {other:?}"),
}
}
#[test]
fn plan_pipeline_defers_stateful_expansion_stages() {
let program = Program::parse("echo ${X:=hi} | upper\n").expect("script should parse");
let pipeline = first_pipeline(&program);
let mut shell = ShellBuilder::new()
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
runtime.register_command("upper", |_argv, _env, _cwd, _stdio| 0);
let plan = Planner::new(&mut shell, &mut runtime).prepare_pipeline(pipeline);
assert!(matches!(
&plan.stages()[1],
PlannedPipelineStage::PreparedExternal(_)
));
match &plan.stages()[0] {
PlannedPipelineStage::Lazy(lazy) => {
assert_eq!(lazy.reason(), DeferredReason::NeedsCurrentShellState);
assert!(matches!(
lazy.deferred_work(),
Some(DeferredPipelineStageWork::Expansion(
DeferredExpansion::ParameterExpansion {
op: mxsh::ast::ParameterOp::Equal,
..
}
))
));
}
other => panic!("expected deferred shell stage, got {other:?}"),
}
}
#[test]
fn plan_pipeline_reports_command_substitution_work() {
let program = Program::parse("echo $(printf hi) | upper\n").expect("script should parse");
let pipeline = first_pipeline(&program);
let mut shell = ShellBuilder::new()
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
runtime.register_command("upper", |_argv, _env, _cwd, _stdio| 0);
let plan = Planner::new(&mut shell, &mut runtime).prepare_pipeline(pipeline);
match &plan.stages()[0] {
PlannedPipelineStage::Lazy(lazy) => assert!(matches!(
lazy.deferred_work(),
Some(DeferredPipelineStageWork::Expansion(
DeferredExpansion::CommandSubstitution { .. }
))
)),
other => panic!("expected deferred shell stage, got {other:?}"),
}
}