Sebastian Thielrefactor8ccd7608 months agoHistory
1676 lines · 62.8 KB · Rust
RawDownload
//! Utilities for testing `gitoxide` crates, many of which might be useful for testing programs that use `git` in general.//!//! ## Environment Variables//!//! ### `GIX_TEST_FIXTURE_HASH`//!//! Set this variable to control which hash function is used when creating or loading test fixtures.//! Valid values are the names of hash functions supported by `gix_hash::Kind` (e.g., `sha1`, `sha256`).//! If not set, the default hash function via `gix_hash::Kind::default()` is used.//! //! ## 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; const ARCHIVE_DIR_NAME: &str = "generated-archives"; /// 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 result type for post-processing closures in `*_with_post` fixture functions.////// The closure can return any value `T`, which will be returned alongside the fixture path./// This is useful for computing values based on the fixture contents.pub type PostResult<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; /// Indicates the state of a fixture when a closure is called.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum FixtureState<'a> {    /// The fixture is newly created and needs post-processing.    ///    /// The closure should perform any necessary modifications to the fixture    /// directory and compute its return value.    Uninitialized(&'a Path),    /// The fixture was already created (cached) and only needs to produce a return value.    ///    /// The closure should NOT modify the fixture directory, but only compute    /// and return a value based on the existing contents.    Fresh(&'a Path),} impl FixtureState<'_> {    /// Returns the path of the fixture, which is always a directory.    pub fn path(&self) -> &Path {        match self {            FixtureState::Uninitialized(path) | FixtureState::Fresh(path) => path,        }    }     /// Returns true if the fixture is uninitialized and needs to be modified.    pub fn is_uninitialized(&self) -> bool {        matches!(self, FixtureState::Uninitialized(_))    }} /// 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()`] and [`rust_fixture_writable()`]/// produces the writable copy.pub enum Creation {    /// Run the code 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 code in the writable location. That way, absolute paths match the location.    Execute,} /// 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, non-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: impl AsRef<Path>) -> 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::<fn(FixtureState<'_>) -> PostResult, ()>(        script_name,        args,        mode,        DirectoryRoot::IntegrationTest,        ArgsInHash::Yes,        None::<(u32, _)>,    )    .map(|(dir, _)| dir)} /// 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::<fn(FixtureState<'_>) -> PostResult, ()>(        script_name,        args,        mode,        DirectoryRoot::IntegrationTest,        ArgsInHash::No,        None::<(u32, _)>,    )    .map(|(dir, _)| dir)} /// 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: impl AsRef<Path>,    args: impl IntoIterator<Item = impl Into<String>>,    mode: Creation,) -> Result<tempfile::TempDir> {    scripted_fixture_writable_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(        script_name,        args,        mode,        DirectoryRoot::StandaloneTest,        ArgsInHash::Yes,        None::<(u32, _)>,    )    .map(|(dir, _)| dir)} /// 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: impl AsRef<Path>,    args: impl IntoIterator<Item = impl Into<String>>,    mode: Creation,) -> Result<tempfile::TempDir> {    scripted_fixture_writable_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(        script_name,        args,        mode,        DirectoryRoot::StandaloneTest,        ArgsInHash::No,        None::<(u32, _)>,    )    .map(|(dir, _)| dir)} fn scripted_fixture_writable_with_args_inner<F, T>(    script_name: impl AsRef<Path>,    args: impl IntoIterator<Item = impl Into<String>>,    mode: Creation,    root: DirectoryRoot,    args_in_hash: ArgsInHash,    mut post_process: Option<(u32, F)>,) -> Result<(tempfile::TempDir, Option<T>)>where    F: FnMut(FixtureState<'_>) -> PostResult<T>,{    let dst = tempfile::TempDir::new()?;    Ok(match mode {        Creation::CopyFromReadOnly => {            // Create the read-only fixture with post_process (modifications are cached)            let (ro_dir, _res_ignored) = scripted_fixture_read_only_with_args_inner(                script_name,                args,                None,                root,                args_in_hash,                post_process.as_mut().map(|(v, f)| (*v, f)),            )?;            copy_recursively_into_existing_dir(ro_dir, dst.path())?;            (dst, _res_ignored)        }        Creation::Execute => {            // Execute directly in the temp dir with post_process            let (_, post_result) = scripted_fixture_read_only_with_args_inner(                script_name,                args,                dst.path().into(),                root,                args_in_hash,                post_process.as_mut().map(|(v, f)| (*v, f)),            )?;            (dst, post_result)        }    })} 

Lines 1–500 of 1676.