mod words;
use std::path::Path;
use std::sync::Arc;
use super::*;
use crate::rig::Shell;
use crate::scratch::accounts;
use bash_strings::emit_array;
fn real() -> [String; 5] {
[
emit_array(&words(&[
"probe", "inner", "outer", "top", "main",
])),
emit_array(&words(&["/x.bash"; 5])),
emit_array(&words(&["9", "10", "11", "12", "0"])),
emit_array(&words(&["1", "2", "3", "1", "0"])),
emit_array(&words(&[
"solo", "i2", "i1", "o3", "o2", "o1", "t1",
])),
]
}
fn words(items: &[&str]) -> Vec<String> {
items.iter().map(ToString::to_string).collect()
}
fn columns(at: &[String; 5], skip: usize, traced: bool) -> Columns<'_> {
Columns {
skip,
pwd: "/w",
funcs: &at[0],
sources: &at[1],
lines: &at[2],
args: traced.then(|| Args {
argc: &at[3],
argv: &at[4],
}),
}
}
fn reading() -> Arc<Shell> {
accounts::reading("/x.bash")
}
fn given() -> [String; 5] {
[
emit_array(&words(&["probe", "inner", "outer"])),
emit_array(&words(&["bash"; 3])),
emit_array(&words(&["9", "10", "3"])),
emit_array(&words(&["1", "2", "3"])),
emit_array(&words(&[
"solo", "i2", "i1", "o3", "o2", "o1",
])),
]
}
#[test]
fn a_walk_comes_back_as_the_calls_that_were_written() {
let raw = real();
let walk = columns(&raw, 1, true).frames(&reading()).unwrap();
let frames: Vec<&Frame> = walk.frames().collect();
assert_eq!(
frames.len(),
4,
"probe's own frame is the instrument's"
);
assert_eq!(
walk.top().to_string(),
"inner@x.bash:9 ('i1' 'i2')",
"where the walk was taken"
);
assert_eq!(
frames[1].to_string(),
"outer@x.bash:10 ('o1' 'o2' 'o3')"
);
assert_eq!(
frames[2].to_string(),
"top@x.bash:11 ('t1')"
);
assert_eq!(
frames[3].args.as_deref(),
Some([].as_slice()),
"called with none"
);
assert_eq!(
frames[3].to_string(),
"main@x.bash:12 ()",
"which prints as none, not absent"
);
}
#[test]
fn skipping_further_drops_frames_without_shifting_them() {
let raw = real();
let one = columns(&raw, 1, true).frames(&reading()).unwrap();
let three = columns(&raw, 3, true).frames(&reading()).unwrap();
assert_eq!(
three.frames().collect::<Vec<_>>(),
one.frames().skip(2).collect::<Vec<_>>(),
"the same frames, two fewer"
);
}
#[test]
fn an_unrecorded_argument_stack_is_absent_not_empty() {
let raw = real();
assert!(
columns(&raw, 1, false)
.frames(&reading())
.unwrap()
.frames()
.all(|f| f.args.is_none())
);
assert_eq!(
columns(&raw, 1, false)
.frames(&reading())
.unwrap()
.top()
.to_string(),
"inner@x.bash:9"
);
}
#[test]
fn a_short_argument_column_is_absent_rather_than_misread() {
let raw = real();
let short = emit_array(&words(&["3", "1", "0"]));
let at = Columns {
args: Some(Args {
argc: &short,
argv: &raw[4],
}),
..columns(&raw, 1, false)
};
assert!(
at.frames(&reading())
.unwrap()
.frames()
.all(|f| f.args.is_none())
);
}
#[test]
fn a_record_that_does_not_line_up_is_refused() {
let raw = real();
let ragged = emit_array(&words(&["a", "b"]));
let uneven = Columns {
sources: &ragged,
..columns(&raw, 1, true)
};
assert!(
uneven.frames(&reading()).is_err(),
"columns of different lengths"
);
let over = emit_array(&words(&["9", "9", "9", "9", "9"]));
let wide = Columns {
args: Some(Args {
argc: &over,
argv: &raw[4],
}),
..columns(&raw, 1, false)
};
assert!(
wide.frames(&reading()).is_err(),
"widths claiming more arguments than there are"
);
assert!(
columns(&raw, 0, true).frames(&reading()).is_err(),
"skip is at least the emitter's own"
);
assert!(
columns(&raw, 6, true).frames(&reading()).is_err(),
"and never past the end"
);
assert!(
columns(&raw, 5, true).frames(&reading()).is_err(),
"a walk with no frames"
);
}
#[test]
fn a_shell_given_no_script_file_ends_at_the_frame_bash_never_pushed() {
let raw = given();
let walk = columns(&raw, 1, true)
.frames(&accounts::given("bash"))
.unwrap();
let frames: Vec<&Frame> = walk.frames().collect();
assert_eq!(
frames.len(),
3,
"inner, outer, and the shell above them"
);
assert_eq!(
walk.top().to_string(),
"inner@-:9 ('i1' 'i2')",
"and $0 is not read as a path"
);
assert_eq!(
frames[1].to_string(),
"outer@-:10 ('o1' 'o2' 'o3')"
);
assert_eq!(
frames[2].to_string(),
"shell@-:3",
"where the walk was entered, args unrecorded"
);
let bare = columns(&raw, 3, true)
.frames(&accounts::given("bash"))
.unwrap();
assert_eq!(bare.frames().count(), 1);
assert_eq!(bare.top().to_string(), "shell@-:3");
}
#[test]
fn bashs_own_words_are_read_as_what_they_are() {
let pwd = Path::new("/w");
assert_eq!(
Site::of("f__A"),
Site::Function("f__A".into())
);
assert_eq!(Site::of("main"), Site::Script);
assert_eq!(Site::of("source"), Site::Sourced);
let read = accounts::reading("run.bash");
assert_eq!(
Source::of("environment", pwd, &read),
Source::Environment
);
assert_eq!(
Source::of("main", pwd, &read),
Source::Prompt
);
assert_eq!(
Source::of("/abs/x.bash", pwd, &read),
Source::File("/abs/x.bash".into())
);
assert_eq!(
Source::of("bash", pwd, &accounts::given("bash")),
Source::Shell
);
assert_eq!(
Source::of("run.bash", pwd, &read),
Source::File("/w/run.bash".into())
);
}
#[test]
fn a_relative_source_joins_the_walk_s_own_directory() {
let pwd = Path::new("/w/here");
assert_eq!(
Source::of("sub/x.bash", pwd, &reading()),
Source::File("/w/here/sub/x.bash".into())
);
assert_eq!(
Source::of("sub/../x.bash", pwd, &reading()),
Source::File("/w/here/sub/../x.bash".into())
);
assert_eq!(
Source::of("x.bash", pwd, &reading()),
Source::File("/w/here/x.bash".into())
);
assert_eq!(
Source::of("/x.bash", pwd, &reading()),
Source::File("/x.bash".into())
);
}
#[test]
fn only_a_file_is_ever_found_or_missing() {
let here = Source::of(
"src/stack/tests/mod.rs",
Path::new(env!("CARGO_MANIFEST_DIR")),
&reading(),
);
let gone = Source::of(
"nowhere/at/all.bash",
Path::new("/w"),
&reading(),
);
assert!(here.found().is_some() && here.missing().is_none());
assert!(gone.found().is_none() && gone.missing() == Some(Path::new("/w/nowhere/at/all.bash")));
for word in [Source::Environment, Source::Prompt, Source::Shell] {
assert!(
word.found().is_none() && word.missing().is_none(),
"{word:?} is not a path"
);
}
}
#[test]
fn the_sections_are_read_off_a_payload() {
let raw = real();
let payload: Vec<String> = [
"skip", "1", "pwd", "/w", "funcs", &raw[0], "sources", &raw[1], "lines", &raw[2], "argc", &raw[3], "argv",
&raw[4],
]
.iter()
.map(ToString::to_string)
.collect();
let read = Columns::of(&payload).unwrap().frames(&reading()).unwrap();
assert_eq!(
read,
columns(&raw, 1, true).frames(&reading()).unwrap()
);
assert!(
Columns::of(&payload[2..]).is_err(),
"no skip"
);
assert!(
Columns::of(&payload[..12]).is_err(),
"argc without argv"
);
}