prek 0.5.2

A fast Git hook manager written in Rust, designed as a drop-in alternative to pre-commit, reimagined.
#[cfg(feature = "ci")]
use anyhow::Result;
#[cfg(feature = "ci")]
use assert_fs::assert::PathAssert;
#[cfg(feature = "ci")]
use assert_fs::fixture::PathChild;
use prek_consts::env_vars::EnvVars;

use crate::common::{TestEnv, cmd_snapshot};

/// Test `language_version` parsing and installation for Rust hooks.
#[cfg(feature = "ci")]
#[test]
fn language_version() -> Result<()> {
    let context = TestEnv::new()
        .with_config(indoc::indoc! {r"
        repos:
          - repo: local
            hooks:
              - id: rust-system
                name: rust-system
                language: rust
                entry: rustc --version
                language_version: system
                pass_filenames: false
                always_run: true
              - id: rust-1.70 # should auto install 1.70.X
                name: rust-1.70
                language: rust
                entry: rustc --version
                language_version: '1.70'
                always_run: true
                pass_filenames: false
              - id: rust-1.70 # run again to ensure reusing the installed version
                name: rust-1.70
                language: rust
                entry: rustc --version
                language_version: '1.70'
                always_run: true
                pass_filenames: false
    "})
        .init_git();

    let rust_dir = context.home_dir().child("tools/rustup/toolchains");
    rust_dir.assert(predicates::path::missing());

    let context = context.with_filters([
        (r"rustc (1\.70)\.\d{1,2} .+", "rustc $1.X"), // Keep 1.70.X format
        (r"rustc 1\.\d{1,3}\.\d{1,2} .+", "rustc 1.X.X"), // Others become 1.X.X
    ]);

    cmd_snapshot!(context, context.run().arg("-v"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    rust-system..............................................................Passed
    - hook id: rust-system
    - duration: [TIME]

      rustc 1.X.X
    rust-1.70................................................................Passed
    - hook id: rust-1.70
    - duration: [TIME]

      rustc 1.70.X
    rust-1.70................................................................Passed
    - hook id: rust-1.70
    - duration: [TIME]

      rustc 1.70.X

    ----- stderr -----
    "#);

    // Ensure that only Rust 1.70.X is installed.
    let installed_versions = rust_dir
        .read_dir()?
        .flatten()
        .filter_map(|d| {
            let filename = d.file_name().to_string_lossy().into_owned();
            if filename.starts_with('.') {
                None
            } else {
                Some(filename)
            }
        })
        .collect::<Vec<_>>();

    assert_eq!(
        installed_versions.len(),
        1,
        "Expected only one Rust version to be installed, but found: {installed_versions:?}"
    );
    assert!(
        installed_versions.iter().any(|v| v.starts_with("1.70")),
        "Expected Rust 1.70.X to be installed, but found: {installed_versions:?}"
    );

    Ok(())
}

/// Test `rustup` installer.
#[test]
fn rustup_installer() {
    let context = TestEnv::new()
        .with_config(indoc::indoc! {r"
        repos:
          - repo: local
            hooks:
              - id: rustup-test
                name: rustup-test
                language: rust
                entry: rustc --version
       "})
        .init_git();

    let context = context.with_filter(r"rustc 1\.\d{1,3}\.\d{1,2} .+", "rustc 1.X.X");

    cmd_snapshot!(context, context.run().arg("-v").env(EnvVars::PREK_INTERNAL__RUSTUP_BINARY_NAME, "non-exist-rustup"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    rustup-test..............................................................Passed
    - hook id: rustup-test
    - duration: [TIME]

      rustc 1.X.X

    ----- stderr -----
    "#);
}

/// Test that `additional_dependencies` with cli: prefix are installed correctly.
#[test]
fn additional_dependencies_cli() {
    let context = TestEnv::new()
        .with_config(indoc::indoc! {r#"
        repos:
          - repo: local
            hooks:
              - id: rust-cli
                name: rust-cli
                language: rust
                entry: prek-rust-echo Hello, Prek!
                additional_dependencies: ["cli:prek-rust-echo"]
                always_run: true
                verbose: true
                pass_filenames: false
    "#})
        .init_git();

    cmd_snapshot!(context, context.run(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    rust-cli.................................................................Passed
    - hook id: rust-cli
    - duration: [TIME]

      Hello, Prek!

    ----- stderr -----
    ");
}

/// Test cargo-binstall is used only for crates.io CLI dependencies.
#[cfg(feature = "ci")]
#[test]
fn cargo_binstall_is_used_only_for_crates_io_cli_dependencies() {
    let context = TestEnv::new()
        .with_file(
            "Cargo.toml",
            indoc::indoc! {r#"
                [package]
                name = "cargo-binstall-test"
                version = "0.1.0"
                edition = "2021"
            "#},
        )
        .init_git();
    let hook_repo = context
        .create_hook_repo(
            "rust-cargo-hook",
            indoc::indoc! {r"
                - id: cargo-hook
                  name: cargo-hook
                  language: rust
                  entry: prek-cargo-hook
            "},
        )
        .with_file(
            "Cargo.toml",
            indoc::indoc! {r#"
                [package]
                name = "prek-cargo-hook"
                version = "0.1.0"
                edition = "2021"
            "#},
        )
        .with_file(
            "Cargo.lock",
            indoc::indoc! {r#"
                # This file is automatically @generated by Cargo.
                # It is not intended for manual editing.
                version = 4

                [[package]]
                name = "prek-cargo-hook"
                version = "0.1.0"
            "#},
        )
        .with_file(
            "src/main.rs",
            indoc::indoc! {r#"
                fn main() {
                    println!("installed from checkout");
                }
            "#},
        )
        .build();

    context.write_config(indoc::formatdoc! {r#"
        repos:
          - repo: {hook_repo}
            rev: v1.0.0
            hooks:
              - id: cargo-hook
                language_version: system
                always_run: true
                verbose: true
                pass_filenames: false
          - repo: local
            hooks:
              - id: binstall-cli
                name: binstall-cli
                language: rust
                entry: cargo-get package.name
                additional_dependencies: ["cli:cargo-get:1.4.0"]
                language_version: system
                always_run: true
                verbose: true
                pass_filenames: false
    "#});
    context.git().add(".");

    cmd_snapshot!(context, context.run()
        .env(EnvVars::PREK_USE_CARGO_BINSTALL, "1"), @r#"
    success: true
    exit_code: 0
    ----- stdout -----
    cargo-hook...............................................................Passed
    - hook id: cargo-hook
    - duration: [TIME]

      installed from checkout
    binstall-cli.............................................................Passed
    - hook id: binstall-cli
    - duration: [TIME]

      cargo-binstall-test

    ----- stderr -----
    "#);
}

/// Test that remote Rust hooks are installed and run correctly.
#[test]
fn remote_hooks() {
    let context = TestEnv::new()
        .with_config(indoc::indoc! {r#"
        repos:
          - repo: https://github.com/prek-ci/rust-hooks
            rev: v1.0.0
            hooks:
              - id: hello-world
                verbose: true
                pass_filenames: false
                always_run: true
                args: ["Hello World"]
    "#})
        .init_git();

    cmd_snapshot!(context, context.run(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    Hello World..............................................................Passed
    - hook id: hello-world
    - description: echoes its arguments
    - duration: [TIME]

      Hello World

    ----- stderr -----
    ");
}

/// Test that remote Rust hooks from non-workspace repos are installed and run correctly.
#[test]
fn remote_hook_non_workspace() {
    let context = TestEnv::new()
        .with_config(indoc::indoc! {r"
        repos:
          - repo: https://github.com/prek-ci/rust-hooks-non-workspace
            rev: v1.0.0
            hooks:
              - id: hello-world
                verbose: true
                pass_filenames: false
                always_run: true
    "})
        .init_git();

    cmd_snapshot!(context, context.run(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    hello-world..............................................................Passed
    - hook id: hello-world
    - description: A simple hello world hook
    - duration: [TIME]

      Hello, Prek!

    ----- stderr -----
    ");
}

/// Test that library dependencies (non-cli: prefix) work correctly on remote hooks.
/// This verifies that the shared repo is not modified when adding dependencies.
#[test]
fn remote_hooks_with_lib_deps() {
    let context = TestEnv::new()
        .with_config(indoc::indoc! {r#"
        repos:
          - repo: https://github.com/prek-ci/rust-hooks
            rev: v1.0.0
            hooks:
              - id: hello-world-lib-deps
                additional_dependencies: ["itoa:1"]
                verbose: true
                pass_filenames: false
                always_run: true
    "#})
        .init_git();

    cmd_snapshot!(context, context.run(), @r"
    success: true
    exit_code: 0
    ----- stdout -----
    Hello World Lib Deps.....................................................Passed
    - hook id: hello-world-lib-deps
    - description: requires a library dependency to be added
    - duration: [TIME]

      42

    ----- stderr -----
    ");
}