#![cfg(all(feature = "embed", feature = "test-support"))]
use mxsh::ShellBuilder;
use mxsh::advanced::{PlannedAndOr, PlannedPipelineStage, Planner};
use mxsh::ast::{AndOrList, Command as AstCommand, ParameterOp, Program, Word};
use mxsh::embed::{DeferredWorkDetail, DeferredWorkKind, StdioConfig, TraceEvent};
use mxsh::runtime::testing::{InMemoryRuntime, StringStdioOut};
fn normalize_trace(mut trace: Vec<TraceEvent>) -> Vec<TraceEvent> {
for event in &mut trace {
match event {
TraceEvent::RunStarted {
timestamp,
command_id,
..
}
| TraceEvent::RunFinished {
timestamp,
command_id,
..
}
| TraceEvent::BackgroundJobStarted {
timestamp,
command_id,
..
}
| TraceEvent::BackgroundJobFinished {
timestamp,
command_id,
..
} => {
assert!(!timestamp.is_empty(), "event timestamp should be populated");
assert!(
!command_id.is_empty(),
"event command id should be populated"
);
*timestamp = "<timestamp>".to_string();
*command_id = "<command-id>".to_string();
}
TraceEvent::DeferredWorkStarted {
timestamp,
command_id,
work_id,
..
}
| TraceEvent::DeferredWorkFinished {
timestamp,
command_id,
work_id,
..
} => {
assert!(!timestamp.is_empty(), "event timestamp should be populated");
assert!(
!command_id.is_empty(),
"event command id should be populated"
);
assert!(!work_id.is_empty(), "event work id should be populated");
*timestamp = "<timestamp>".to_string();
*command_id = "<command-id>".to_string();
*work_id = "<work-id>".to_string();
}
}
}
trace
}
fn first_stage_argument_words(program: &Program) -> Vec<Word> {
program
.body()
.iter()
.map(|command_list| {
let AndOrList::Pipeline(pipeline) = command_list.and_or_list() else {
panic!("expected pipeline");
};
let AstCommand::Simple(command) = &pipeline.commands()[0] else {
panic!("expected simple command");
};
command.arguments()[0].clone()
})
.collect()
}
#[test]
fn run_outcome_receives_run_lifecycle_trace_events() {
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.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");
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "hello\n");
assert_eq!(
normalize_trace(result.trace),
vec![
TraceEvent::RunStarted {
timestamp: "<timestamp>".to_string(),
pid: std::process::id(),
command_id: "<command-id>".to_string(),
raw_command: Some("echo hello".to_string()),
canonical_command: Some("echo hello".to_string()),
},
TraceEvent::RunFinished {
timestamp: "<timestamp>".to_string(),
pid: std::process::id(),
command_id: "<command-id>".to_string(),
status: 0,
},
]
);
}
#[test]
fn run_outcome_receives_deferred_pipeline_work_trace() {
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
runtime.register_command("cat", |_argv, _env, _cwd, stdio| {
let input = stdio.stdin_fd.dup().expect("dup cat stdin").read_all();
let _ = stdio.stdout_fd.write_str(&input);
0
});
let result = shell.run(&mut runtime, "unset X; echo ${X:=hi} | cat");
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "hi\n");
assert_eq!(result.trace.len(), 4);
assert!(matches!(&result.trace[0], TraceEvent::RunStarted { .. }));
assert!(matches!(
&result.trace[1],
TraceEvent::DeferredWorkStarted {
kind: DeferredWorkKind::ParameterExpansion,
..
}
));
assert!(matches!(
&result.trace[2],
TraceEvent::DeferredWorkFinished {
kind: DeferredWorkKind::ParameterExpansion,
status: 0,
..
}
));
assert!(matches!(&result.trace[3], TraceEvent::RunFinished { .. }));
}
#[test]
fn executing_planned_program_preserves_work_ids() {
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
runtime.register_command("cat", |_argv, _env, _cwd, stdio| {
let input = stdio.stdin_fd.dup().expect("dup cat stdin").read_all();
let _ = stdio.stdout_fd.write_str(&input);
0
});
let program =
Program::parse("echo ${A:=one} | cat; echo ${B:=two} | cat").expect("script should parse");
let plan = Planner::new(&mut shell, &mut runtime).prepare(&program);
let expected_work_ids: Vec<String> = plan
.command_lists()
.iter()
.map(|command_list| match command_list.and_or_list() {
PlannedAndOr::Pipeline(pipeline) => match &pipeline.stages()[0] {
PlannedPipelineStage::Lazy(lazy) => lazy
.work_id()
.expect("expected deferred stage work id")
.to_string(),
other => panic!("expected deferred stage, got {other:?}"),
},
other => panic!("expected pipeline, got {other:?}"),
})
.collect();
let result = Planner::new(&mut shell, &mut runtime).execute_plan(&plan);
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "one\ntwo\n");
let actual_work_ids: Vec<String> = result
.trace
.iter()
.filter_map(|event| match event {
TraceEvent::DeferredWorkStarted { work_id, .. } => Some(work_id.clone()),
_ => None,
})
.collect();
assert_eq!(actual_work_ids, expected_work_ids);
}
#[test]
fn run_outcome_preserves_deferred_work_details() {
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
runtime.register_command("cat", |_argv, _env, _cwd, stdio| {
let input = stdio.stdin_fd.dup().expect("dup cat stdin").read_all();
let _ = stdio.stdout_fd.write_str(&input);
0
});
let program = Program::parse("echo ${A:=one} | cat").expect("script should parse");
let expected_word = first_stage_argument_words(&program)
.into_iter()
.next()
.expect("expected a first stage argument");
let result = shell.run(&mut runtime, "echo ${A:=one} | cat");
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "one\n");
let start = result
.trace
.iter()
.find_map(|event| match event {
TraceEvent::DeferredWorkStarted {
command_id,
work_id,
kind,
detail,
..
} => Some((command_id.clone(), work_id.clone(), *kind, detail.clone())),
_ => None,
})
.expect("expected deferred-work start event");
let finish = result
.trace
.iter()
.find_map(|event| match event {
TraceEvent::DeferredWorkFinished {
command_id,
work_id,
kind,
detail,
status,
..
} => Some((
command_id.clone(),
work_id.clone(),
*kind,
detail.clone(),
*status,
)),
_ => None,
})
.expect("expected deferred-work finish event");
assert_eq!(start.0, finish.0);
assert_eq!(start.1, finish.1);
assert_eq!(start.2, DeferredWorkKind::ParameterExpansion);
assert_eq!(finish.2, DeferredWorkKind::ParameterExpansion);
assert_eq!(finish.4, 0);
assert_eq!(
start.3,
DeferredWorkDetail::ParameterExpansion {
word: expected_word.clone(),
op: ParameterOp::Equal,
}
);
assert_eq!(
finish.3,
DeferredWorkDetail::ParameterExpansion {
word: expected_word,
op: ParameterOp::Equal,
}
);
}