//! Utilities for testing `gitoxide` crates, many of which might be useful for testing programs that use `git` in general.//!//! ## Feature Flags#![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!())]#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]#![deny(missing_docs)] use std::{ collections::BTreeMap, env, ffi::{OsStr, OsString}, io::Read, path::{Path, PathBuf}, str::FromStr, time::Duration,}; pub use bstr;use bstr::ByteSlice;use io_close::Close;pub use is_ci;use parking_lot::Mutex;use std::sync::LazyLock;pub use tempfile; /// A result type to allow using the try operator `?` in unit tests.////// Use it like so:////// ```no_run/// use gix_testtools::Result;////// #[test]/// fn this() -> Result {/// let x: usize = "42".parse()?;/// Ok(())////// }/// ```pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; /// A wrapper for a running git-daemon process which is killed automatically on drop.////// Note that we will swallow any errors, assuming that the test would have failed if the daemon crashed.pub struct GitDaemon { child: std::process::Child, /// The base url under which all repositories are hosted, typically `git://127.0.0.1:port`. pub url: String,} impl Drop for GitDaemon { fn drop(&mut self) { self.child.kill().ok(); }} static SCRIPT_IDENTITY: LazyLock<Mutex<BTreeMap<PathBuf, u32>>> = LazyLock::new(|| Mutex::new(BTreeMap::new())); static EXCLUDE_LUT: LazyLock<Mutex<Option<gix_worktree::Stack>>> = LazyLock::new(|| { let cache = (|| { let (repo_path, _) = gix_discover::upwards(Path::new(".")).ok()?; let (gix_dir, work_tree) = repo_path.into_repository_and_work_tree_directories(); let work_tree = work_tree?.canonicalize().ok()?; let mut buf = Vec::with_capacity(512); let case = if gix_fs::Capabilities::probe(&work_tree).ignore_case { gix_worktree::ignore::glob::pattern::Case::Fold } else { Default::default() }; let state = gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new( Default::default(), gix_worktree::ignore::Search::from_git_dir( &gix_dir, None, &mut buf, gix_worktree::stack::state::ignore::ParseIgnore { support_precious: false, }, ) .ok()?, None, gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, Default::default(), )); Some(gix_worktree::Stack::new( work_tree, state, case, buf, Default::default(), )) })(); Mutex::new(cache)}); #[cfg(windows)]const GIT_PROGRAM: &str = "git.exe";#[cfg(not(windows))]const GIT_PROGRAM: &str = "git"; static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| { let output = std::process::Command::new(GIT_PROGRAM) .arg("--exec-path") .output() .expect("can execute `git --exec-path`"); assert!(output.status.success(), "`git --exec-path` failed"); output .stdout .strip_suffix(b"\n") .expect("`git --exec-path` output to be well-formed") .to_os_str() .expect("no invalid UTF-8 in `--exec-path` except as OS allows") .into()}); /// The major, minor and patch level of the git version on the system.pub static GIT_VERSION: LazyLock<(u8, u8, u8)> = LazyLock::new(|| parse_git_version().expect("git version to be parsable")); /// Define how [`scripted_fixture_writable_with_args()`] uses produces the writable copy.pub enum Creation { /// Run the script once and copy the data from its output to the writable location. /// This is fast but won't work if absolute paths are produced by the script. /// /// ### Limitation /// /// Cannot handle symlinks currently. Waiting for [this PR](https://github.com/webdesus/fs_extra/pull/70). CopyFromReadOnly, /// Run the script in the writable location. That way, absolute paths match the location. ExecuteScript,} /// Returns true if the given `major`, `minor` and `patch` is smaller than the actual git version on the system/// to facilitate skipping a test on the caller./// Will never return true on CI which is expected to have a recent enough git version.////// # Panics////// If `git` cannot be executed or if its version output cannot be parsed.pub fn should_skip_as_git_version_is_smaller_than(major: u8, minor: u8, patch: u8) -> bool { if is_ci::cached() { return false; // CI should be made to use a recent git version, it should run there. } *GIT_VERSION < (major, minor, patch)} fn parse_git_version() -> Result<(u8, u8, u8)> { let output = std::process::Command::new(GIT_PROGRAM).arg("--version").output()?; git_version_from_bytes(&output.stdout)} fn git_version_from_bytes(bytes: &[u8]) -> Result<(u8, u8, u8)> { let mut numbers = bytes .split(|b| *b == b' ' || *b == b'\n') .nth(2) .expect("git version <version>") .split(|b| *b == b'.') .take(3) .map(|n| std::str::from_utf8(n).expect("valid utf8 in version number")) .map(u8::from_str); Ok((|| -> Result<_> { Ok(( numbers.next().expect("major")?, numbers.next().expect("minor")?, numbers.next().expect("patch")?, )) })() .map_err(|err| { format!( "Could not parse version from output of 'git --version' ({:?}) with error: {}", bytes.to_str_lossy(), err ) })?)} /// Set the current working dir to `new_cwd` and return a type that returns to the previous working dir on drop.pub fn set_current_dir(new_cwd: impl AsRef<Path>) -> std::io::Result<AutoRevertToPreviousCWD> { let cwd = env::current_dir()?; env::set_current_dir(new_cwd)?; Ok(AutoRevertToPreviousCWD(cwd))} /// A utility to set the current working dir to the given value, on drop.////// # Panics////// Note that this will panic if the CWD cannot be set on drop.#[derive(Debug)]#[must_use]pub struct AutoRevertToPreviousCWD(PathBuf); impl Drop for AutoRevertToPreviousCWD { fn drop(&mut self) { env::set_current_dir(&self.0).unwrap(); }} /// Run `git` in `working_dir` with all provided `args`.pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> { std::process::Command::new(GIT_PROGRAM) .current_dir(working_dir) .args(args) .status()} /// Spawn a git daemon process to host all repository at or below `working_dir`.pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> { let mut ports: Vec<_> = (9419u16..9419 + 100).collect(); fastrand::shuffle(&mut ports); let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port)); let free_port = { let listener = std::net::TcpListener::bind(ports.into_iter().map(addr_at).collect::<Vec<_>>().as_slice())?; listener.local_addr().expect("listener address is available").port() }; let child = std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" })) .current_dir(working_dir) .args(["--verbose", "--base-path=.", "--export-all", "--user-path"]) .arg(format!("--port={free_port}")) .spawn()?; let server_addr = addr_at(free_port); for time in gix_lock::backoff::Quadratic::default_with_random() { std::thread::sleep(time); if std::net::TcpStream::connect(server_addr).is_ok() { break; } } Ok(GitDaemon { child, url: format!("git://{server_addr}"), })} #[derive(Copy, Clone)]enum DirectoryRoot { IntegrationTest, StandaloneTest,} /// Don't add a suffix to the archive name as `args` are platform dependent, none-deterministic,/// or otherwise don't influence the content of the archive./// Note that this also means that `args` won't be used to control the hash of the archive itself.#[derive(Copy, Clone)]enum ArgsInHash { Yes, No,} /// Return the path to the `<crate-root>/tests/fixtures/<path>` directory.pub fn fixture_path(path: impl AsRef<Path>) -> PathBuf { fixture_path_inner(path, DirectoryRoot::IntegrationTest)} /// Return the path to the `<crate-root>/fixtures/<path>` directory.pub fn fixture_path_standalone(path: impl AsRef<Path>) -> PathBuf { fixture_path_inner(path, DirectoryRoot::StandaloneTest)}/// Return the path to the `<crate-root>/tests/fixtures/<path>` directory.fn fixture_path_inner(path: impl AsRef<Path>, root: DirectoryRoot) -> PathBuf { match root { DirectoryRoot::StandaloneTest => PathBuf::from("fixtures").join(path.as_ref()), DirectoryRoot::IntegrationTest => PathBuf::from("tests").join("fixtures").join(path.as_ref()), }} /// Load the fixture from `<crate-root>/tests/fixtures/<path>` and return its data, or _panic_.pub fn fixture_bytes(path: impl AsRef<Path>) -> Vec<u8> { fixture_bytes_inner(path, DirectoryRoot::IntegrationTest)} /// Like [`scripted_fixture_writable`], but does not prefix the fixture directory with `tests`pub fn fixture_bytes_standalone(path: impl AsRef<Path>) -> Vec<u8> { fixture_bytes_inner(path, DirectoryRoot::StandaloneTest)} fn fixture_bytes_inner(path: impl AsRef<Path>, root: DirectoryRoot) -> Vec<u8> { match std::fs::read(fixture_path_inner(path.as_ref(), root)) { Ok(res) => res, Err(_) => panic!("File at '{}' not found", path.as_ref().display()), }} /// Run the executable at `script_name`, like `make_repo.sh` or `my_setup.py` to produce a read-only directory to which/// the path is returned.////// Note that it persists and the script at `script_name` will only be executed once if it ran without error.////// ### Automatic Archive Creation////// In order to speed up CI and even local runs should the cache get purged, the result of each script run/// is automatically placed into a compressed _tar_ archive./// If a script result doesn't exist, these will be checked first and extracted if present, which they are by default./// This behaviour can be prohibited by setting the `GIX_TEST_IGNORE_ARCHIVES` to any value.////// To speed CI up, one can add these archives to the repository. Since LFS is not currently being used, it is/// important to check their size first, though in most cases generated archives will not be very large.////// #### Disable Archive Creation////// If archives aren't useful, they can be disabled by using `.gitignore` specifications./// That way it's trivial to prevent creation of all archives with `generated-archives/*.tar{.xz}` in the root/// or more specific `.gitignore` configurations in lower levels of the work tree.////// The latter is useful if the script's output is platform specific.pub fn scripted_fixture_read_only(script_name: impl AsRef<Path>) -> Result<PathBuf> { scripted_fixture_read_only_with_args(script_name, None::<String>)} /// Like [`scripted_fixture_read_only`], but does not prefix the fixture directory with `tests`pub fn scripted_fixture_read_only_standalone(script_name: impl AsRef<Path>) -> Result<PathBuf> { scripted_fixture_read_only_with_args_standalone(script_name, None::<String>)} /// Run the executable at `script_name`, like `make_repo.sh` to produce a writable directory to which/// the tempdir is returned. It will be removed automatically, courtesy of [`tempfile::TempDir`].////// Note that `script_name` is only executed once, so the data can be copied from its read-only location.pub fn scripted_fixture_writable(script_name: impl AsRef<Path>) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args(script_name, None::<String>, Creation::CopyFromReadOnly)} /// Like [`scripted_fixture_writable`], but does not prefix the fixture directory with `tests`pub fn scripted_fixture_writable_standalone(script_name: &str) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_standalone(script_name, None::<String>, Creation::CopyFromReadOnly)} /// Like [`scripted_fixture_writable()`], but passes `args` to `script_name` while providing control over/// the way files are created with `mode`.pub fn scripted_fixture_writable_with_args( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation,) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::IntegrationTest, ArgsInHash::Yes)} /// Like [`scripted_fixture_writable()`], but passes `args` to `script_name` while providing control over/// the way files are created with `mode`.////// See [`scripted_fixture_read_only_with_args_single_archive()`] for important details on what `single_archive` means.pub fn scripted_fixture_writable_with_args_single_archive( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation,) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::IntegrationTest, ArgsInHash::No)} /// Like [`scripted_fixture_writable_with_args`], but does not prefix the fixture directory with `tests`pub fn scripted_fixture_writable_with_args_standalone( script_name: &str, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation,) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::StandaloneTest, ArgsInHash::Yes)} /// Like [`scripted_fixture_writable_with_args`], but does not prefix the fixture directory with `tests`////// See [`scripted_fixture_read_only_with_args_single_archive()`] for important details on what `single_archive` means.pub fn scripted_fixture_writable_with_args_standalone_single_archive( script_name: &str, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation,) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::StandaloneTest, ArgsInHash::No)} fn scripted_fixture_writable_with_args_inner( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation, root: DirectoryRoot, args_in_hash: ArgsInHash,) -> Result<tempfile::TempDir> { let dst = tempfile::TempDir::new()?; Ok(match mode { Creation::CopyFromReadOnly => { let ro_dir = scripted_fixture_read_only_with_args_inner(script_name, args, None, root, args_in_hash)?; copy_recursively_into_existing_dir(ro_dir, dst.path())?; dst } Creation::ExecuteScript => { scripted_fixture_read_only_with_args_inner(script_name, args, dst.path().into(), root, args_in_hash)?; dst } })} /// A utility to copy the entire contents of `src_dir` into `dst_dir`.pub fn copy_recursively_into_existing_dir(src_dir: impl AsRef<Path>, dst_dir: impl AsRef<Path>) -> std::io::Result<()> { fs_extra::copy_items( &std::fs::read_dir(src_dir)? .map(|e| e.map(|e| e.path())) .collect::<std::result::Result<Vec<_>, _>>()?, dst_dir, &fs_extra::dir::CopyOptions { overwrite: false, skip_exist: false, copy_inside: false, content_only: false, ..Default::default() }, ) .map_err(std::io::Error::other)?; Ok(())} /// Like `scripted_fixture_read_only()`], but passes `args` to `script_name`.pub fn scripted_fixture_read_only_with_args( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>,) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::IntegrationTest, ArgsInHash::Yes)} /// Like `scripted_fixture_read_only()`], but passes `args` to `script_name`.////// Also, don't add a suffix to the archive name as `args` are platform dependent, none-deterministic,/// or otherwise don't influence the content of the archive./// Note that this also means that `args` won't be used to control the hash of the archive itself.////// Sometimes, this should be combined with adding the archive name to `.gitignore` to prevent its creation/// in the first place.////// Note that suffixing archives by default helps to learn what calls are made, and forces the author to/// think about what should be done to get it right.pub fn scripted_fixture_read_only_with_args_single_archive( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>,) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::IntegrationTest, ArgsInHash::No)} /// Like [`scripted_fixture_read_only_with_args()`], but does not prefix the fixture directory with `tests`pub fn scripted_fixture_read_only_with_args_standalone( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>,) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::StandaloneTest, ArgsInHash::Yes)} /// Like [`scripted_fixture_read_only_with_args_standalone()`], only has a single archive.pub fn scripted_fixture_read_only_with_args_standalone_single_archive( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>,) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::StandaloneTest, ArgsInHash::No)} fn scripted_fixture_read_only_with_args_inner( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, destination_dir: Option<&Path>, root: DirectoryRoot, args_in_hash: ArgsInHash,) -> Result<PathBuf> { // Assure tempfiles get removed when aborting the test. gix_tempfile::signal::setup( gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour, ); let script_location = script_name.as_ref(); let script_path = fixture_path_inner(script_location, root); // keep this lock to assure we don't return unfinished directories for threaded callers let args: Vec<String> = args.into_iter().map(Into::into).collect(); let script_identity = { let mut map = SCRIPT_IDENTITY.lock(); map.entry(args.iter().fold(script_path.clone(), |p, a| p.join(a))) .or_insert_with(|| { let crc_value = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM); let mut crc_digest = crc_value.digest(); crc_digest.update(&std::fs::read(&script_path).unwrap_or_else(|err| { panic!( "file {script_path} in CWD '{cwd}' could not be read: {err}", cwd = env::current_dir().expect("valid cwd").display(), script_path = script_path.display(), ) })); for arg in &args { crc_digest.update(arg.as_bytes()); } crc_digest.finalize() }) .to_owned() }; let script_basename = script_location.file_stem().unwrap_or(script_location.as_os_str()); let archive_file_path = fixture_path_inner(Lines 1–500 of 1135. Load more
//! Utilities for testing `gitoxide` crates, many of which might be useful for testing programs that use `git` in general. //! //! ## Feature Flags #![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs)] use std::{ collections::BTreeMap, env, ffi::{OsStr, OsString}, io::Read, path::{Path, PathBuf}, str::FromStr, time::Duration, }; pub use bstr; use bstr::ByteSlice; use io_close::Close; pub use is_ci; use parking_lot::Mutex; use std::sync::LazyLock; pub use tempfile; /// A result type to allow using the try operator `?` in unit tests. /// /// Use it like so: /// /// ```no_run /// use gix_testtools::Result; /// /// #[test] /// fn this() -> Result { /// let x: usize = "42".parse()?; /// Ok(()) /// /// } /// ``` pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; /// A wrapper for a running git-daemon process which is killed automatically on drop. /// /// Note that we will swallow any errors, assuming that the test would have failed if the daemon crashed. pub struct GitDaemon { child: std::process::Child, /// The base url under which all repositories are hosted, typically `git://127.0.0.1:port`. pub url: String, } impl Drop for GitDaemon { fn drop(&mut self) { self.child.kill().ok(); } } static SCRIPT_IDENTITY: LazyLock<Mutex<BTreeMap<PathBuf, u32>>> = LazyLock::new(|| Mutex::new(BTreeMap::new())); static EXCLUDE_LUT: LazyLock<Mutex<Option<gix_worktree::Stack>>> = LazyLock::new(|| { let cache = (|| { let (repo_path, _) = gix_discover::upwards(Path::new(".")).ok()?; let (gix_dir, work_tree) = repo_path.into_repository_and_work_tree_directories(); let work_tree = work_tree?.canonicalize().ok()?; let mut buf = Vec::with_capacity(512); let case = if gix_fs::Capabilities::probe(&work_tree).ignore_case { gix_worktree::ignore::glob::pattern::Case::Fold } else { Default::default() }; let state = gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new( Default::default(), gix_worktree::ignore::Search::from_git_dir( &gix_dir, None, &mut buf, gix_worktree::stack::state::ignore::ParseIgnore { support_precious: false, }, ) .ok()?, None, gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, Default::default(), )); Some(gix_worktree::Stack::new( work_tree, state, case, buf, Default::default(), )) })(); Mutex::new(cache) }); #[cfg(windows)] const GIT_PROGRAM: &str = "git.exe"; #[cfg(not(windows))] const GIT_PROGRAM: &str = "git"; static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| { let output = std::process::Command::new(GIT_PROGRAM) .arg("--exec-path") .output() .expect("can execute `git --exec-path`"); assert!(output.status.success(), "`git --exec-path` failed"); output .stdout .strip_suffix(b"\n") .expect("`git --exec-path` output to be well-formed") .to_os_str() .expect("no invalid UTF-8 in `--exec-path` except as OS allows") .into() }); /// The major, minor and patch level of the git version on the system. pub static GIT_VERSION: LazyLock<(u8, u8, u8)> = LazyLock::new(|| parse_git_version().expect("git version to be parsable")); /// Define how [`scripted_fixture_writable_with_args()`] uses produces the writable copy. pub enum Creation { /// Run the script once and copy the data from its output to the writable location. /// This is fast but won't work if absolute paths are produced by the script. /// /// ### Limitation /// /// Cannot handle symlinks currently. Waiting for [this PR](https://github.com/webdesus/fs_extra/pull/70). CopyFromReadOnly, /// Run the script in the writable location. That way, absolute paths match the location. ExecuteScript, } /// Returns true if the given `major`, `minor` and `patch` is smaller than the actual git version on the system /// to facilitate skipping a test on the caller. /// Will never return true on CI which is expected to have a recent enough git version. /// /// # Panics /// /// If `git` cannot be executed or if its version output cannot be parsed. pub fn should_skip_as_git_version_is_smaller_than(major: u8, minor: u8, patch: u8) -> bool { if is_ci::cached() { return false; // CI should be made to use a recent git version, it should run there. } *GIT_VERSION < (major, minor, patch) } fn parse_git_version() -> Result<(u8, u8, u8)> { let output = std::process::Command::new(GIT_PROGRAM).arg("--version").output()?; git_version_from_bytes(&output.stdout) } fn git_version_from_bytes(bytes: &[u8]) -> Result<(u8, u8, u8)> { let mut numbers = bytes .split(|b| *b == b' ' || *b == b'\n') .nth(2) .expect("git version <version>") .split(|b| *b == b'.') .take(3) .map(|n| std::str::from_utf8(n).expect("valid utf8 in version number")) .map(u8::from_str); Ok((|| -> Result<_> { Ok(( numbers.next().expect("major")?, numbers.next().expect("minor")?, numbers.next().expect("patch")?, )) })() .map_err(|err| { format!( "Could not parse version from output of 'git --version' ({:?}) with error: {}", bytes.to_str_lossy(), err ) })?) } /// Set the current working dir to `new_cwd` and return a type that returns to the previous working dir on drop. pub fn set_current_dir(new_cwd: impl AsRef<Path>) -> std::io::Result<AutoRevertToPreviousCWD> { let cwd = env::current_dir()?; env::set_current_dir(new_cwd)?; Ok(AutoRevertToPreviousCWD(cwd)) } /// A utility to set the current working dir to the given value, on drop. /// /// # Panics /// /// Note that this will panic if the CWD cannot be set on drop. #[derive(Debug)] #[must_use] pub struct AutoRevertToPreviousCWD(PathBuf); impl Drop for AutoRevertToPreviousCWD { fn drop(&mut self) { env::set_current_dir(&self.0).unwrap(); } } /// Run `git` in `working_dir` with all provided `args`. pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> { std::process::Command::new(GIT_PROGRAM) .current_dir(working_dir) .args(args) .status() } /// Spawn a git daemon process to host all repository at or below `working_dir`. pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> { let mut ports: Vec<_> = (9419u16..9419 + 100).collect(); fastrand::shuffle(&mut ports); let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port)); let free_port = { let listener = std::net::TcpListener::bind(ports.into_iter().map(addr_at).collect::<Vec<_>>().as_slice())?; listener.local_addr().expect("listener address is available").port() }; let child = std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" })) .current_dir(working_dir) .args(["--verbose", "--base-path=.", "--export-all", "--user-path"]) .arg(format!("--port={free_port}")) .spawn()?; let server_addr = addr_at(free_port); for time in gix_lock::backoff::Quadratic::default_with_random() { std::thread::sleep(time); if std::net::TcpStream::connect(server_addr).is_ok() { break; } } Ok(GitDaemon { child, url: format!("git://{server_addr}"), }) } #[derive(Copy, Clone)] enum DirectoryRoot { IntegrationTest, StandaloneTest, } /// Don't add a suffix to the archive name as `args` are platform dependent, none-deterministic, /// or otherwise don't influence the content of the archive. /// Note that this also means that `args` won't be used to control the hash of the archive itself. #[derive(Copy, Clone)] enum ArgsInHash { Yes, No, } /// Return the path to the `<crate-root>/tests/fixtures/<path>` directory. pub fn fixture_path(path: impl AsRef<Path>) -> PathBuf { fixture_path_inner(path, DirectoryRoot::IntegrationTest) } /// Return the path to the `<crate-root>/fixtures/<path>` directory. pub fn fixture_path_standalone(path: impl AsRef<Path>) -> PathBuf { fixture_path_inner(path, DirectoryRoot::StandaloneTest) } /// Return the path to the `<crate-root>/tests/fixtures/<path>` directory. fn fixture_path_inner(path: impl AsRef<Path>, root: DirectoryRoot) -> PathBuf { match root { DirectoryRoot::StandaloneTest => PathBuf::from("fixtures").join(path.as_ref()), DirectoryRoot::IntegrationTest => PathBuf::from("tests").join("fixtures").join(path.as_ref()), } } /// Load the fixture from `<crate-root>/tests/fixtures/<path>` and return its data, or _panic_. pub fn fixture_bytes(path: impl AsRef<Path>) -> Vec<u8> { fixture_bytes_inner(path, DirectoryRoot::IntegrationTest) } /// Like [`scripted_fixture_writable`], but does not prefix the fixture directory with `tests` pub fn fixture_bytes_standalone(path: impl AsRef<Path>) -> Vec<u8> { fixture_bytes_inner(path, DirectoryRoot::StandaloneTest) } fn fixture_bytes_inner(path: impl AsRef<Path>, root: DirectoryRoot) -> Vec<u8> { match std::fs::read(fixture_path_inner(path.as_ref(), root)) { Ok(res) => res, Err(_) => panic!("File at '{}' not found", path.as_ref().display()), } } /// Run the executable at `script_name`, like `make_repo.sh` or `my_setup.py` to produce a read-only directory to which /// the path is returned. /// /// Note that it persists and the script at `script_name` will only be executed once if it ran without error. /// /// ### Automatic Archive Creation /// /// In order to speed up CI and even local runs should the cache get purged, the result of each script run /// is automatically placed into a compressed _tar_ archive. /// If a script result doesn't exist, these will be checked first and extracted if present, which they are by default. /// This behaviour can be prohibited by setting the `GIX_TEST_IGNORE_ARCHIVES` to any value. /// /// To speed CI up, one can add these archives to the repository. Since LFS is not currently being used, it is /// important to check their size first, though in most cases generated archives will not be very large. /// /// #### Disable Archive Creation /// /// If archives aren't useful, they can be disabled by using `.gitignore` specifications. /// That way it's trivial to prevent creation of all archives with `generated-archives/*.tar{.xz}` in the root /// or more specific `.gitignore` configurations in lower levels of the work tree. /// /// The latter is useful if the script's output is platform specific. pub fn scripted_fixture_read_only(script_name: impl AsRef<Path>) -> Result<PathBuf> { scripted_fixture_read_only_with_args(script_name, None::<String>) } /// Like [`scripted_fixture_read_only`], but does not prefix the fixture directory with `tests` pub fn scripted_fixture_read_only_standalone(script_name: impl AsRef<Path>) -> Result<PathBuf> { scripted_fixture_read_only_with_args_standalone(script_name, None::<String>) } /// Run the executable at `script_name`, like `make_repo.sh` to produce a writable directory to which /// the tempdir is returned. It will be removed automatically, courtesy of [`tempfile::TempDir`]. /// /// Note that `script_name` is only executed once, so the data can be copied from its read-only location. pub fn scripted_fixture_writable(script_name: impl AsRef<Path>) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args(script_name, None::<String>, Creation::CopyFromReadOnly) } /// Like [`scripted_fixture_writable`], but does not prefix the fixture directory with `tests` pub fn scripted_fixture_writable_standalone(script_name: &str) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_standalone(script_name, None::<String>, Creation::CopyFromReadOnly) } /// Like [`scripted_fixture_writable()`], but passes `args` to `script_name` while providing control over /// the way files are created with `mode`. pub fn scripted_fixture_writable_with_args( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation, ) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::IntegrationTest, ArgsInHash::Yes) } /// Like [`scripted_fixture_writable()`], but passes `args` to `script_name` while providing control over /// the way files are created with `mode`. /// /// See [`scripted_fixture_read_only_with_args_single_archive()`] for important details on what `single_archive` means. pub fn scripted_fixture_writable_with_args_single_archive( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation, ) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::IntegrationTest, ArgsInHash::No) } /// Like [`scripted_fixture_writable_with_args`], but does not prefix the fixture directory with `tests` pub fn scripted_fixture_writable_with_args_standalone( script_name: &str, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation, ) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::StandaloneTest, ArgsInHash::Yes) } /// Like [`scripted_fixture_writable_with_args`], but does not prefix the fixture directory with `tests` /// /// See [`scripted_fixture_read_only_with_args_single_archive()`] for important details on what `single_archive` means. pub fn scripted_fixture_writable_with_args_standalone_single_archive( script_name: &str, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation, ) -> Result<tempfile::TempDir> { scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::StandaloneTest, ArgsInHash::No) } fn scripted_fixture_writable_with_args_inner( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, mode: Creation, root: DirectoryRoot, args_in_hash: ArgsInHash, ) -> Result<tempfile::TempDir> { let dst = tempfile::TempDir::new()?; Ok(match mode { Creation::CopyFromReadOnly => { let ro_dir = scripted_fixture_read_only_with_args_inner(script_name, args, None, root, args_in_hash)?; copy_recursively_into_existing_dir(ro_dir, dst.path())?; dst } Creation::ExecuteScript => { scripted_fixture_read_only_with_args_inner(script_name, args, dst.path().into(), root, args_in_hash)?; dst } }) } /// A utility to copy the entire contents of `src_dir` into `dst_dir`. pub fn copy_recursively_into_existing_dir(src_dir: impl AsRef<Path>, dst_dir: impl AsRef<Path>) -> std::io::Result<()> { fs_extra::copy_items( &std::fs::read_dir(src_dir)? .map(|e| e.map(|e| e.path())) .collect::<std::result::Result<Vec<_>, _>>()?, dst_dir, &fs_extra::dir::CopyOptions { overwrite: false, skip_exist: false, copy_inside: false, content_only: false, ..Default::default() }, ) .map_err(std::io::Error::other)?; Ok(()) } /// Like `scripted_fixture_read_only()`], but passes `args` to `script_name`. pub fn scripted_fixture_read_only_with_args( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, ) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::IntegrationTest, ArgsInHash::Yes) } /// Like `scripted_fixture_read_only()`], but passes `args` to `script_name`. /// /// Also, don't add a suffix to the archive name as `args` are platform dependent, none-deterministic, /// or otherwise don't influence the content of the archive. /// Note that this also means that `args` won't be used to control the hash of the archive itself. /// /// Sometimes, this should be combined with adding the archive name to `.gitignore` to prevent its creation /// in the first place. /// /// Note that suffixing archives by default helps to learn what calls are made, and forces the author to /// think about what should be done to get it right. pub fn scripted_fixture_read_only_with_args_single_archive( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, ) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::IntegrationTest, ArgsInHash::No) } /// Like [`scripted_fixture_read_only_with_args()`], but does not prefix the fixture directory with `tests` pub fn scripted_fixture_read_only_with_args_standalone( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, ) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::StandaloneTest, ArgsInHash::Yes) } /// Like [`scripted_fixture_read_only_with_args_standalone()`], only has a single archive. pub fn scripted_fixture_read_only_with_args_standalone_single_archive( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, ) -> Result<PathBuf> { scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::StandaloneTest, ArgsInHash::No) } fn scripted_fixture_read_only_with_args_inner( script_name: impl AsRef<Path>, args: impl IntoIterator<Item = impl Into<String>>, destination_dir: Option<&Path>, root: DirectoryRoot, args_in_hash: ArgsInHash, ) -> Result<PathBuf> { // Assure tempfiles get removed when aborting the test. gix_tempfile::signal::setup( gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour, ); let script_location = script_name.as_ref(); let script_path = fixture_path_inner(script_location, root); // keep this lock to assure we don't return unfinished directories for threaded callers let args: Vec<String> = args.into_iter().map(Into::into).collect(); let script_identity = { let mut map = SCRIPT_IDENTITY.lock(); map.entry(args.iter().fold(script_path.clone(), |p, a| p.join(a))) .or_insert_with(|| { let crc_value = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM); let mut crc_digest = crc_value.digest(); crc_digest.update(&std::fs::read(&script_path).unwrap_or_else(|err| { panic!( "file {script_path} in CWD '{cwd}' could not be read: {err}", cwd = env::current_dir().expect("valid cwd").display(), script_path = script_path.display(), ) })); for arg in &args { crc_digest.update(arg.as_bytes()); } crc_digest.finalize() }) .to_owned() }; let script_basename = script_location.file_stem().unwrap_or(script_location.as_os_str()); let archive_file_path = fixture_path_inner(