1135 lines · 44.1 KB · Rust
RawDownload
//! 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.