#![allow(clippy::join_absolute_paths)]use std::path::{Path, PathBuf}; use gix_fs::Stack; #[derive(Debug, Default, Eq, PartialEq)]struct Record { push_dir_count: usize, dirs: Vec<PathBuf>, push: usize,} impl gix_fs::stack::Delegate for Record { fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { self.push_dir_count += 1; self.dirs.push(stack.current().into()); Ok(()) } fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> { self.push += 1; Ok(()) } fn pop_directory(&mut self) { self.dirs.pop(); }} #[derive(Default)]struct FailOnce { directory_to_fail_on: Option<PathBuf>, path_to_fail_on: Option<PathBuf>, failed: bool, directories: Vec<PathBuf>, popped_directories: Vec<PathBuf>,} impl gix_fs::stack::Delegate for FailOnce { fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { if !self.failed && self.directory_to_fail_on.as_deref() == Some(stack.current_relative()) { self.failed = true; return Err(std::io::Error::other("failed to push directory")); } self.directories.push(stack.current_relative().to_owned()); Ok(()) } fn push(&mut self, _is_last_component: bool, stack: &Stack) -> std::io::Result<()> { if !self.failed && self.path_to_fail_on.as_deref() == Some(stack.current_relative()) { self.failed = true; return Err(std::io::Error::other("failed to push")); } Ok(()) } fn pop_directory(&mut self) { self.popped_directories .push(self.directories.pop().expect("directory to pop")); }} fn p(s: &str) -> &Path { s.as_ref()} /// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`.#[test]#[cfg(windows)]fn path_join_handling() { let looks_absolute = p("/absolute"); assert!( looks_absolute.is_relative(), "on Windows, 'absolute' Linux paths are relative (and relative to the current drive)" ); let bs_looks_absolute = p(r"\absolute"); assert!( bs_looks_absolute.is_relative(), "on Windows, strange single-backslash paths are relative (and relative to the current drive)" ); assert_eq!( p("relative").join(looks_absolute), looks_absolute, "relative + unix-absolute = unix-absolute - the relative path without a drive is replaced" ); assert_eq!( p("relative").join(bs_looks_absolute), bs_looks_absolute, "relative + unix-absolute = unix-absolute - the relative path without a drive is replaced - backslashes aren't special here" ); assert_eq!( p("c:").join("relative"), p("c:relative"), "drive + relative = relative to the drive-specific current directory" ); assert_eq!( p(r"c:\").join("relative"), p(r"c:\relative"), "absolute + relative = joined result" ); assert_eq!( p(r"\\?\base").join(looks_absolute), p(r"\\?\base\absolute"), "absolute1 + unix-absolute2 = joined result with backslash" ); assert_eq!( p(r"\\.\base").join(looks_absolute), p(r"\\.\base\absolute"), "absolute1 + absolute2 = joined result with backslash (device namespace)" ); assert_eq!( p(r"\\?\base").join(bs_looks_absolute), p(r"\\?\base\absolute"), "absolute1 + absolute2 = joined result" ); assert_eq!( p(r"\\.\base").join(bs_looks_absolute), p(r"\\.\base\absolute"), "absolute1 + absolute2 = joined result (device namespace)" ); assert_eq!(p("/").join("C:"), p("C:"), "unix-absolute + win-drive = win-drive"); assert_eq!( p("d:/").join("C:"), p("C:"), "d-drive + c-drive-relative = c-drive-relative - C: is relative but not on D:" ); assert_eq!( p(r"d:\").join(r"C:\"), p(r"C:\"), "d-drive-with-bs + c-drive-with-bs = c-drive-with-bs - nothing special happens with backslashes" ); assert_eq!( p(r"c:\").join(r"\\.\"), p(r"\\.\"), "c-drive-with-bs + device-namespace-unc = device-namespace-unc" ); assert_eq!( p("/").join("C:/"), p(r"C:\"), "unix-absolute + win-drive = win-drive, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes" ); assert_eq!(p("/").join(r"C:\"), p(r"C:\"), "unix-absolute + win-drive = win-drive"); assert_eq!( p(r"\\.").join("C:"), p("C:"), r"device-namespace-unc + win-drive-relative = win-drive-relative - C: as a relative path is not the C: device, so this is not \\.\C:" ); assert_eq!(p("relative").join("C:"), p("C:"), "relative + win-drive = win-drive"); assert_eq!( p("/").join(r"\\localhost"), p(r"\localhost"), "unix-absolute + win-absolute-unc-host = strangely, single-backslashed host" ); assert_eq!( p("relative").join(r"\\localhost"), p(r"\\localhost"), "relative + win-absolute-unc-host = win-absolute-unc-host" );} /// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`.#[test]#[cfg(not(windows))]fn path_join_handling() { assert_eq!( p("relative").join("/absolute"), p("/absolute"), "relative + absolute = absolute" ); assert_eq!( p("/").join("relative"), p("/relative"), "absolute + relative = joined result" ); assert_eq!( p("/").join("/absolute"), p("/absolute"), "absolute1 + absolute2 = absolute2" ); assert_eq!(p("/").join("C:"), p("/C:"), "absolute + win-drive = joined result"); assert_eq!(p("/").join("C:/"), p("/C:/"), "absolute + win-absolute = joined result"); assert_eq!( p("/").join(r"C:\"), p(r"/C:\"), "absolute + win-absolute = joined result" ); assert_eq!( p("relative").join("C:"), p("relative/C:"), "relative + win-drive = joined result" ); assert_eq!( p("/").join(r"\\localhost"), p(r"/\\localhost"), "absolute + win-absolute-unc-host = joined result" ); assert_eq!( p("relative").join(r"\\localhost"), p(r"relative/\\localhost"), "relative + win-absolute-unc-host = joined result" );} #[test]fn empty_paths_are_noop_if_no_path_was_pushed_before() { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); let mut r = Record::default(); s.make_relative_path_current("", &mut r).unwrap(); assert_eq!( s.current_relative().to_string_lossy(), "", "it's fine to push an empty path to get a value for the stack root, once" );} #[test]fn relative_components_are_invalid() { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); let mut r = Record::default(); let err = s.make_relative_path_current(p("a/.."), &mut r).unwrap_err(); assert_eq!( err.to_string(), format!( "Input path {input:?} contains relative or absolute components", input = "a/.." ) ); s.make_relative_path_current(p("a/./b"), &mut r) .expect("dot is ignored"); assert_eq!( r, Record { push_dir_count: 2, dirs: vec![".".into(), "./a".into()], push: 2, }, "The `a` directory is pushed, and the leaf, for a total of 2 pushes" ); assert_eq!( s.current().to_string_lossy(), if cfg!(windows) { r".\a\b" } else { "./a/b" }, "dot is silently ignored" ); s.make_relative_path_current(p("a//b/"), &mut r) .expect("multiple-slashes are ignored"); assert_eq!( r, Record { push_dir_count: 2, dirs: vec![".".into(), "./a".into()], push: 2, }, "nothing changed" );} #[test]fn absolute_paths_are_invalid() -> crate::Result { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); let mut r = Record::default(); let err = s.make_relative_path_current(p("/"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "/" contains relative or absolute components"#, "a leading slash is always considered absolute" ); s.make_relative_path_current("/", &mut r)?; assert_eq!( s.current(), s.root(), "as string this is a no-op as it's just split by /" ); let err = s.make_relative_path_current("../breakout", &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "../breakout" contains relative or absolute components"#, "otherwise breakout attempts are detected" ); s.make_relative_path_current(p("a/"), &mut r)?; assert_eq!( s.current(), p("./a/"), "trailing slashes aren't a problem at this stage, as they cannot cause a 'breakout'" ); s.make_relative_path_current(p(r"b\"), &mut r)?; assert_eq!( s.current(), p(r"./b\"), "trailing backslashes are fine both on Windows and Unix - on Unix it's part of the filename" ); #[cfg(windows)] { let err = s.make_relative_path_current(Path::new(r"\"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "\" contains relative or absolute components"#, "on Windows, backslashes are considered absolute and replace the base if it is relative, \ hence they are forbidden." ); let err = s.make_relative_path_current(Path::new("c:"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "c:" contains relative or absolute components"#, "on Windows, drive-letters without trailing backslash or slash are also absolute (even though they ought to be relative)" ); let err = s.make_relative_path_current(Path::new(r"c:\"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "c:\" contains relative or absolute components"#, "on Windows, drive-letters are absolute, which is expected" ); s.make_relative_path_current(Path::new("֍:"), &mut r)?; assert_eq!( s.current().to_string_lossy(), ".\\֍:", "on Windows, almost any unicode character will do as virtual drive-letter actually with `subst`, \ but we just turn it into a presumably invalid path which is fine, i.e. we get a joined path" ); let err = s .make_relative_path_current(Path::new(r"\\localhost\hello"), &mut r) .unwrap_err(); assert_eq!( err.to_string(), r#"Input path "\\localhost\hello" contains relative or absolute components"#, "there is UNC paths as well" ); let err = s .make_relative_path_current(Path::new(r#"\\?\C:"#), &mut r) .unwrap_err(); assert_eq!( err.to_string(), r#"Input path "\\?\C:" contains relative or absolute components"#, "there is UNC paths as well, sometimes they look different" ); } Ok(())} #[test]fn delegate_calls_are_consistent() -> crate::Result { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); assert_eq!(s.current(), root); assert_eq!(s.current_relative(), p("")); let mut r = Record::default(); s.make_relative_path_current("a/b", &mut r)?; let mut dirs = vec![root.clone(), root.join("a")]; assert_eq!( r, Record { push_dir_count: 2, dirs: dirs.clone(), push: 2, }, "it pushes the root-directory first, then the intermediate one" ); s.make_relative_path_current("a/b2", &mut r)?; assert_eq!( r, Record { push_dir_count: 2, dirs: dirs.clone(), push: 3, }, "dirs remain the same as b2 is a leaf/file, hence the new `push`" ); s.make_relative_path_current("c/d/e", &mut r)?; dirs.pop(); dirs.extend([root.join("c"), root.join("c").join("d")]); assert_eq!( r, Record { push_dir_count: 4, dirs: dirs.clone(), push: 6, }, "each directory is pushed individually, after popping 'a' which isn't included anymore" ); dirs.push(root.join("c").join("d").join("x")); s.make_relative_path_current("c/d/x/z", &mut r)?; assert_eq!( r, Record { push_dir_count: 5, dirs: dirs.clone(), push: 8, }, "a new path component is added, hence `push_dir + 1`, but two components are added in total" ); dirs.drain(1..).count(); s.make_relative_path_current("f", &mut r)?; assert_eq!(s.current_relative(), p("f")); assert_eq!( r, Record { push_dir_count: 5, dirs: dirs.clone(), push: 9, }, "Now we only keep the root, as `f` is a leaf, hence `push + 1`" ); dirs.push(root.join("x")); s.make_relative_path_current("x/z", &mut r)?; assert_eq!( r, Record { push_dir_count: 6, dirs: dirs.clone(), push: 11, }, "a new directory is pushed, or two new components total, hence `push + 2`" ); dirs.push(root.join("x").join("z")); s.make_relative_path_current("x/z/a", &mut r)?; assert_eq!( r, Record { push_dir_count: 7, dirs: dirs.clone(), push: 13, }, "and another sub-directory is added after revalidating the cached leaf as a directory" ); dirs.push(root.join("x").join("z").join("a")); dirs.push(root.join("x").join("z").join("a").join("b")); s.make_relative_path_current("x/z/a/b/c", &mut r)?; assert_eq!( r, Record { push_dir_count: 9, dirs: dirs.clone(), push: 16, }, "and more subdirectories, two at once this time, after revalidating the cached leaf as a directory." ); dirs.drain(1 /*root*/ + 1 /*x*/ + 1 /*x/z*/ ..).count(); s.make_relative_path_current("x/z", &mut r)?; assert_eq!( r, Record { push_dir_count: 9, dirs: dirs.clone(), push: 16, }, "this only pops components, and as x/z/a/ was previously a directory, x/z is still a directory" ); assert_eq!( dirs.last(), Some(&PathBuf::from("./x/z")), "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." ); let err = s.make_relative_path_current(p(""), &mut r).unwrap_err(); assert_eq!( err.to_string(), "empty inputs are not allowed", "this is to protect us from double-counting the root path next time a component is pushed, \ and besides that really shouldn't happen" ); s.make_relative_path_current("leaf", &mut r)?; dirs.drain(1..).count(); assert_eq!( r, Record { push_dir_count: 9, dirs: dirs.clone(), push: 17, }, "reset as much as possible, with just a leaf-component and the root directory"Lines 1–500 of 645. Load more
#![allow(clippy::join_absolute_paths)] use std::path::{Path, PathBuf}; use gix_fs::Stack; #[derive(Debug, Default, Eq, PartialEq)] struct Record { push_dir_count: usize, dirs: Vec<PathBuf>, push: usize, } impl gix_fs::stack::Delegate for Record { fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { self.push_dir_count += 1; self.dirs.push(stack.current().into()); Ok(()) } fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> { self.push += 1; Ok(()) } fn pop_directory(&mut self) { self.dirs.pop(); } } #[derive(Default)] struct FailOnce { directory_to_fail_on: Option<PathBuf>, path_to_fail_on: Option<PathBuf>, failed: bool, directories: Vec<PathBuf>, popped_directories: Vec<PathBuf>, } impl gix_fs::stack::Delegate for FailOnce { fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { if !self.failed && self.directory_to_fail_on.as_deref() == Some(stack.current_relative()) { self.failed = true; return Err(std::io::Error::other("failed to push directory")); } self.directories.push(stack.current_relative().to_owned()); Ok(()) } fn push(&mut self, _is_last_component: bool, stack: &Stack) -> std::io::Result<()> { if !self.failed && self.path_to_fail_on.as_deref() == Some(stack.current_relative()) { self.failed = true; return Err(std::io::Error::other("failed to push")); } Ok(()) } fn pop_directory(&mut self) { self.popped_directories .push(self.directories.pop().expect("directory to pop")); } } fn p(s: &str) -> &Path { s.as_ref() } /// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`. #[test] #[cfg(windows)] fn path_join_handling() { let looks_absolute = p("/absolute"); assert!( looks_absolute.is_relative(), "on Windows, 'absolute' Linux paths are relative (and relative to the current drive)" ); let bs_looks_absolute = p(r"\absolute"); assert!( bs_looks_absolute.is_relative(), "on Windows, strange single-backslash paths are relative (and relative to the current drive)" ); assert_eq!( p("relative").join(looks_absolute), looks_absolute, "relative + unix-absolute = unix-absolute - the relative path without a drive is replaced" ); assert_eq!( p("relative").join(bs_looks_absolute), bs_looks_absolute, "relative + unix-absolute = unix-absolute - the relative path without a drive is replaced - backslashes aren't special here" ); assert_eq!( p("c:").join("relative"), p("c:relative"), "drive + relative = relative to the drive-specific current directory" ); assert_eq!( p(r"c:\").join("relative"), p(r"c:\relative"), "absolute + relative = joined result" ); assert_eq!( p(r"\\?\base").join(looks_absolute), p(r"\\?\base\absolute"), "absolute1 + unix-absolute2 = joined result with backslash" ); assert_eq!( p(r"\\.\base").join(looks_absolute), p(r"\\.\base\absolute"), "absolute1 + absolute2 = joined result with backslash (device namespace)" ); assert_eq!( p(r"\\?\base").join(bs_looks_absolute), p(r"\\?\base\absolute"), "absolute1 + absolute2 = joined result" ); assert_eq!( p(r"\\.\base").join(bs_looks_absolute), p(r"\\.\base\absolute"), "absolute1 + absolute2 = joined result (device namespace)" ); assert_eq!(p("/").join("C:"), p("C:"), "unix-absolute + win-drive = win-drive"); assert_eq!( p("d:/").join("C:"), p("C:"), "d-drive + c-drive-relative = c-drive-relative - C: is relative but not on D:" ); assert_eq!( p(r"d:\").join(r"C:\"), p(r"C:\"), "d-drive-with-bs + c-drive-with-bs = c-drive-with-bs - nothing special happens with backslashes" ); assert_eq!( p(r"c:\").join(r"\\.\"), p(r"\\.\"), "c-drive-with-bs + device-namespace-unc = device-namespace-unc" ); assert_eq!( p("/").join("C:/"), p(r"C:\"), "unix-absolute + win-drive = win-drive, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes" ); assert_eq!(p("/").join(r"C:\"), p(r"C:\"), "unix-absolute + win-drive = win-drive"); assert_eq!( p(r"\\.").join("C:"), p("C:"), r"device-namespace-unc + win-drive-relative = win-drive-relative - C: as a relative path is not the C: device, so this is not \\.\C:" ); assert_eq!(p("relative").join("C:"), p("C:"), "relative + win-drive = win-drive"); assert_eq!( p("/").join(r"\\localhost"), p(r"\localhost"), "unix-absolute + win-absolute-unc-host = strangely, single-backslashed host" ); assert_eq!( p("relative").join(r"\\localhost"), p(r"\\localhost"), "relative + win-absolute-unc-host = win-absolute-unc-host" ); } /// Just to learn the specialities of `Path::join()`, which boils down to `Path::push(component)`. #[test] #[cfg(not(windows))] fn path_join_handling() { assert_eq!( p("relative").join("/absolute"), p("/absolute"), "relative + absolute = absolute" ); assert_eq!( p("/").join("relative"), p("/relative"), "absolute + relative = joined result" ); assert_eq!( p("/").join("/absolute"), p("/absolute"), "absolute1 + absolute2 = absolute2" ); assert_eq!(p("/").join("C:"), p("/C:"), "absolute + win-drive = joined result"); assert_eq!(p("/").join("C:/"), p("/C:/"), "absolute + win-absolute = joined result"); assert_eq!( p("/").join(r"C:\"), p(r"/C:\"), "absolute + win-absolute = joined result" ); assert_eq!( p("relative").join("C:"), p("relative/C:"), "relative + win-drive = joined result" ); assert_eq!( p("/").join(r"\\localhost"), p(r"/\\localhost"), "absolute + win-absolute-unc-host = joined result" ); assert_eq!( p("relative").join(r"\\localhost"), p(r"relative/\\localhost"), "relative + win-absolute-unc-host = joined result" ); } #[test] fn empty_paths_are_noop_if_no_path_was_pushed_before() { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); let mut r = Record::default(); s.make_relative_path_current("", &mut r).unwrap(); assert_eq!( s.current_relative().to_string_lossy(), "", "it's fine to push an empty path to get a value for the stack root, once" ); } #[test] fn relative_components_are_invalid() { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); let mut r = Record::default(); let err = s.make_relative_path_current(p("a/.."), &mut r).unwrap_err(); assert_eq!( err.to_string(), format!( "Input path {input:?} contains relative or absolute components", input = "a/.." ) ); s.make_relative_path_current(p("a/./b"), &mut r) .expect("dot is ignored"); assert_eq!( r, Record { push_dir_count: 2, dirs: vec![".".into(), "./a".into()], push: 2, }, "The `a` directory is pushed, and the leaf, for a total of 2 pushes" ); assert_eq!( s.current().to_string_lossy(), if cfg!(windows) { r".\a\b" } else { "./a/b" }, "dot is silently ignored" ); s.make_relative_path_current(p("a//b/"), &mut r) .expect("multiple-slashes are ignored"); assert_eq!( r, Record { push_dir_count: 2, dirs: vec![".".into(), "./a".into()], push: 2, }, "nothing changed" ); } #[test] fn absolute_paths_are_invalid() -> crate::Result { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); let mut r = Record::default(); let err = s.make_relative_path_current(p("/"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "/" contains relative or absolute components"#, "a leading slash is always considered absolute" ); s.make_relative_path_current("/", &mut r)?; assert_eq!( s.current(), s.root(), "as string this is a no-op as it's just split by /" ); let err = s.make_relative_path_current("../breakout", &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "../breakout" contains relative or absolute components"#, "otherwise breakout attempts are detected" ); s.make_relative_path_current(p("a/"), &mut r)?; assert_eq!( s.current(), p("./a/"), "trailing slashes aren't a problem at this stage, as they cannot cause a 'breakout'" ); s.make_relative_path_current(p(r"b\"), &mut r)?; assert_eq!( s.current(), p(r"./b\"), "trailing backslashes are fine both on Windows and Unix - on Unix it's part of the filename" ); #[cfg(windows)] { let err = s.make_relative_path_current(Path::new(r"\"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "\" contains relative or absolute components"#, "on Windows, backslashes are considered absolute and replace the base if it is relative, \ hence they are forbidden." ); let err = s.make_relative_path_current(Path::new("c:"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "c:" contains relative or absolute components"#, "on Windows, drive-letters without trailing backslash or slash are also absolute (even though they ought to be relative)" ); let err = s.make_relative_path_current(Path::new(r"c:\"), &mut r).unwrap_err(); assert_eq!( err.to_string(), r#"Input path "c:\" contains relative or absolute components"#, "on Windows, drive-letters are absolute, which is expected" ); s.make_relative_path_current(Path::new("֍:"), &mut r)?; assert_eq!( s.current().to_string_lossy(), ".\\֍:", "on Windows, almost any unicode character will do as virtual drive-letter actually with `subst`, \ but we just turn it into a presumably invalid path which is fine, i.e. we get a joined path" ); let err = s .make_relative_path_current(Path::new(r"\\localhost\hello"), &mut r) .unwrap_err(); assert_eq!( err.to_string(), r#"Input path "\\localhost\hello" contains relative or absolute components"#, "there is UNC paths as well" ); let err = s .make_relative_path_current(Path::new(r#"\\?\C:"#), &mut r) .unwrap_err(); assert_eq!( err.to_string(), r#"Input path "\\?\C:" contains relative or absolute components"#, "there is UNC paths as well, sometimes they look different" ); } Ok(()) } #[test] fn delegate_calls_are_consistent() -> crate::Result { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); assert_eq!(s.current(), root); assert_eq!(s.current_relative(), p("")); let mut r = Record::default(); s.make_relative_path_current("a/b", &mut r)?; let mut dirs = vec![root.clone(), root.join("a")]; assert_eq!( r, Record { push_dir_count: 2, dirs: dirs.clone(), push: 2, }, "it pushes the root-directory first, then the intermediate one" ); s.make_relative_path_current("a/b2", &mut r)?; assert_eq!( r, Record { push_dir_count: 2, dirs: dirs.clone(), push: 3, }, "dirs remain the same as b2 is a leaf/file, hence the new `push`" ); s.make_relative_path_current("c/d/e", &mut r)?; dirs.pop(); dirs.extend([root.join("c"), root.join("c").join("d")]); assert_eq!( r, Record { push_dir_count: 4, dirs: dirs.clone(), push: 6, }, "each directory is pushed individually, after popping 'a' which isn't included anymore" ); dirs.push(root.join("c").join("d").join("x")); s.make_relative_path_current("c/d/x/z", &mut r)?; assert_eq!( r, Record { push_dir_count: 5, dirs: dirs.clone(), push: 8, }, "a new path component is added, hence `push_dir + 1`, but two components are added in total" ); dirs.drain(1..).count(); s.make_relative_path_current("f", &mut r)?; assert_eq!(s.current_relative(), p("f")); assert_eq!( r, Record { push_dir_count: 5, dirs: dirs.clone(), push: 9, }, "Now we only keep the root, as `f` is a leaf, hence `push + 1`" ); dirs.push(root.join("x")); s.make_relative_path_current("x/z", &mut r)?; assert_eq!( r, Record { push_dir_count: 6, dirs: dirs.clone(), push: 11, }, "a new directory is pushed, or two new components total, hence `push + 2`" ); dirs.push(root.join("x").join("z")); s.make_relative_path_current("x/z/a", &mut r)?; assert_eq!( r, Record { push_dir_count: 7, dirs: dirs.clone(), push: 13, }, "and another sub-directory is added after revalidating the cached leaf as a directory" ); dirs.push(root.join("x").join("z").join("a")); dirs.push(root.join("x").join("z").join("a").join("b")); s.make_relative_path_current("x/z/a/b/c", &mut r)?; assert_eq!( r, Record { push_dir_count: 9, dirs: dirs.clone(), push: 16, }, "and more subdirectories, two at once this time, after revalidating the cached leaf as a directory." ); dirs.drain(1 /*root*/ + 1 /*x*/ + 1 /*x/z*/ ..).count(); s.make_relative_path_current("x/z", &mut r)?; assert_eq!( r, Record { push_dir_count: 9, dirs: dirs.clone(), push: 16, }, "this only pops components, and as x/z/a/ was previously a directory, x/z is still a directory" ); assert_eq!( dirs.last(), Some(&PathBuf::from("./x/z")), "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." ); let err = s.make_relative_path_current(p(""), &mut r).unwrap_err(); assert_eq!( err.to_string(), "empty inputs are not allowed", "this is to protect us from double-counting the root path next time a component is pushed, \ and besides that really shouldn't happen" ); s.make_relative_path_current("leaf", &mut r)?; dirs.drain(1..).count(); assert_eq!( r, Record { push_dir_count: 9, dirs: dirs.clone(), push: 17, }, "reset as much as possible, with just a leaf-component and the root directory"