1
0
mirror of https://github.com/j178/prek.git synced 2026-04-25 02:11:36 +02:00
Files
prek/tests/run.rs
Les Freire ed3f357f85 Support language: lua hooks (#954)
* Add Lua language support

Refactor Lua integration: streamline async functions and improve rockspec file handling

Add Lua and LuaRocks installation steps to CI workflow

Add MSVC development command to CI workflow

Update CI workflow to specify Windows-only dependencies for MSVC and Lua installations

Update CI workflow to use official Lua and LuaRocks GitHub actions

Update CI workflow to specify Windows-only dependencies for Lua and LuaRocks installations

Update CI workflow to use PowerShell for Cargo test execution and improve command formatting

Add platform-specific command snapshots for Lua tests

Enhance the Lua test environment by adding separate command snapshots for Windows and non-Windows platforms. This ensures accurate output handling based on the operating system, improving test reliability and clarity.

Update CI workflow to use specific versions of Lua and LuaRocks actions

Refactor Lua dependency installation and update test cases

Refactor Lua command execution to streamline environment path handling

Fix dependencies installing

Fix remote repo hook

* Add lua remote_hook test

* Tweaks

---------

Co-authored-by: Jo <10510431+j178@users.noreply.github.com>
2025-10-24 19:36:38 +08:00

2426 lines
69 KiB
Rust

use std::path::Path;
use std::process::Command;
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::prelude::*;
use constants::env_vars::EnvVars;
use constants::{ALT_CONFIG_FILE, CONFIG_FILE};
use insta::assert_snapshot;
use predicates::prelude::predicate;
use crate::common::{TestContext, cmd_snapshot};
mod common;
#[test]
fn run_basic() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
"});
// Create a repository with some files.
cwd.child("file.txt").write_str("Hello, world!\n")?;
cwd.child("valid.json").write_str("{}")?;
cwd.child("invalid.json").write_str("{}")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing main.py
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing valid.json
Fixing invalid.json
Fixing main.py
check json...............................................................Passed
----- stderr -----
"#);
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("trailing-whitespace"), @r#"
success: true
exit_code: 0
----- stdout -----
trim trailing whitespace.................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn run_in_non_git_repo() {
let context = TestContext::new();
let mut filters = context.filters();
filters.push((r"exit code: ", "exit status: "));
cmd_snapshot!(filters, context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: command `get git root` exited with an error:
[status]
exit status: 128
[stderr]
fatal: not a git repository (or any of the parent directories): .git
");
}
#[test]
fn invalid_config() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config("invalid: config");
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `.pre-commit-config.yaml`
caused by: missing field `repos`
"#);
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: dotnet
additional_dependencies: ["dotnet@6"]
entry: echo Hello, world!
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Hook `trailing-whitespace` is invalid
caused by: Hook specified `additional_dependencies: dotnet@6` but the language `dotnet` does not support installing dependencies for now
");
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: fail
language_version: '6'
entry: echo Hello, world!
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Hook `trailing-whitespace` is invalid
caused by: Hook specified `language_version: 6` but the language `fail` does not support toolchain installation for now
");
}
/// Use same repo multiple times, with same or different revisions.
#[test]
fn same_repo() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
"});
cwd.child("file.txt").write_str("Hello, world!\n")?;
cwd.child("valid.json").write_str("{}")?;
cwd.child("invalid.json").write_str("{}")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing main.py
trim trailing whitespace.................................................Passed
trim trailing whitespace.................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn local() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: local
name: local
language: system
entry: echo Hello, world!
always_run: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
local....................................................................Passed
----- stderr -----
"#);
}
/// Test multiple hook IDs scenarios.
#[test]
fn multiple_hook_ids() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: hook1
name: First Hook
language: system
entry: echo hook1
- id: hook2
name: Second Hook
language: system
entry: echo hook2
- id: shared-name
name: Shared Hook A
language: system
entry: echo shared-a
- id: shared-name-2
name: Shared Hook B
language: system
entry: echo shared-b
alias: shared-name
"});
context.git_add(".");
// Multiple repeated hook-id (should deduplicate)
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("hook1").arg("hook1"), @r#"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
----- stderr -----
"#);
// Hook-id that matches multiple hooks (by alias)
cmd_snapshot!(context.filters(), context.run().arg("shared-name"), @r#"
success: true
exit_code: 0
----- stdout -----
Shared Hook A............................................................Passed
Shared Hook B............................................................Passed
----- stderr -----
"#);
// Hook-id matches nothing
cmd_snapshot!(context.filters(), context.run().arg("nonexistent-hook"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: selector `nonexistent-hook` did not match any hooks
error: No hooks found after filtering with the given selectors
");
// Multiple hook_ids match nothing
cmd_snapshot!(context.filters(), context.run().arg("nonexistent-hook").arg("nonexistent-hook").arg("nonexistent-hook-2"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: the following selectors did not match any hooks or projects:
- `nonexistent-hook`
- `nonexistent-hook-2`
error: No hooks found after filtering with the given selectors
");
// Hook-id matches one hook
cmd_snapshot!(context.filters(), context.run().arg("hook2"), @r#"
success: true
exit_code: 0
----- stdout -----
Second Hook..............................................................Passed
----- stderr -----
"#);
// Multiple hook-ids with mixed results (some exist, some don't)
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("nonexistent").arg("hook2"), @r"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
Second Hook..............................................................Passed
----- stderr -----
warning: selector `nonexistent` did not match any hooks
");
// Multiple valid hook-ids
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("hook2").arg("nonexistent-hook"), @r"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
Second Hook..............................................................Passed
----- stderr -----
warning: selector `nonexistent-hook` did not match any hooks
");
// Multiple hook-ids with some duplicates and aliases
cmd_snapshot!(context.filters(), context.run().arg("hook1").arg("shared-name").arg("hook1"), @r#"
success: true
exit_code: 0
----- stdout -----
First Hook...............................................................Passed
Shared Hook A............................................................Passed
Shared Hook B............................................................Passed
----- stderr -----
"#);
}
/// `.pre-commit-config.yaml` is not staged.
#[test]
fn config_not_staged() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.work_dir().child(CONFIG_FILE).touch()?;
context.git_add(".");
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -V
"});
cmd_snapshot!(context.filters(), context.run().arg("invalid-hook-id"), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: prek configuration file is not staged, run `git add .pre-commit-config.yaml` to stage it
"#);
Ok(())
}
/// `.pre-commit-config.yaml` outside the repository should not be checked.
#[test]
fn config_outside_repo() -> Result<()> {
let context = TestContext::new();
// Initialize a git repository in ./work.
let root = context.work_dir().child("work");
root.create_dir_all()?;
Command::new("git")
.arg("init")
.current_dir(&root)
.assert()
.success();
// Create a configuration file in . (outside the repository).
context
.work_dir()
.child("c.yaml")
.write_str(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Hello world")'
"#})?;
cmd_snapshot!(context.filters(), context.run().current_dir(&root).arg("-c").arg("../c.yaml"), @r#"
success: true
exit_code: 0
----- stdout -----
trailing-whitespace..................................(no files to check)Skipped
----- stderr -----
"#);
Ok(())
}
/// Test the output format for a hook with a CJK name.
#[test]
fn cjk_hook_name() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: 去除行尾空格
language: system
entry: python3 -V
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -V
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
去除行尾空格.............................................................Passed
fix end of files.........................................................Passed
----- stderr -----
"#);
}
/// Skips hooks based on the `SKIP` environment variable.
#[test]
fn skips() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c "exit(1)"
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -c "exit(1)"
- id: check-json
name: check json
language: system
entry: python3 -c "exit(1)"
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().env("SKIP", "end-of-file-fixer"), @r"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
check json...............................................................Failed
- hook id: check-json
- exit code: 1
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("SKIP", "trailing-whitespace,end-of-file-fixer"), @r"
success: false
exit_code: 1
----- stdout -----
check json...............................................................Failed
- hook id: check-json
- exit code: 1
----- stderr -----
");
}
/// Run hooks with matched `stage`.
#[test]
fn stage() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: manual-stage
name: manual-stage
language: system
entry: echo manual-stage
stages: [ manual ]
# Defaults to all stages.
- id: default-stage
name: default-stage
language: system
entry: echo default-stage
- id: post-commit-stage
name: post-commit-stage
language: system
entry: echo post-commit-stage
stages: [ post-commit ]
"});
context.git_add(".");
// By default, run hooks with `pre-commit` stage.
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
default-stage............................................................Passed
----- stderr -----
"#);
// Run hooks with `manual` stage.
cmd_snapshot!(context.filters(), context.run().arg("--hook-stage").arg("manual"), @r#"
success: true
exit_code: 0
----- stdout -----
manual-stage.............................................................Passed
default-stage............................................................Passed
----- stderr -----
"#);
// Run hooks with `post-commit` stage.
cmd_snapshot!(context.filters(), context.run().arg("--hook-stage").arg("post-commit"), @r#"
success: true
exit_code: 0
----- stdout -----
default-stage........................................(no files to check)Skipped
post-commit-stage....................................(no files to check)Skipped
----- stderr -----
"#);
}
/// Test global `files`, `exclude`, and hook level `files`, `exclude`.
#[test]
fn files_and_exclude() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
cwd.child("file.txt").write_str("Hello, world! \n")?;
cwd.child("valid.json").write_str("{}\n ")?;
cwd.child("invalid.json").write_str("{}")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
// Global files and exclude.
context.write_pre_commit_config(indoc::indoc! {r"
files: file.txt
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: [text]
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: [text]
- id: check-json
name: check json
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: [json]
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trailing whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['file.txt']
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
['file.txt']
check json...........................................(no files to check)Skipped
----- stderr -----
"#);
// Override hook level files and exclude.
context.write_pre_commit_config(indoc::indoc! {r"
files: file.txt
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
files: valid.json
- id: end-of-file-fixer
name: fix end of files
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
exclude: (valid.json|main.py)
- id: check-json
name: check json
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trailing whitespace..................................(no files to check)Skipped
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
['file.txt']
check json...............................................................Failed
- hook id: check-json
- exit code: 1
['file.txt']
----- stderr -----
"#);
Ok(())
}
/// Test selecting files by type, `types`, `types_or`, and `exclude_types`.
#[test]
fn file_types() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
cwd.child("file.txt").write_str("Hello, world! ")?;
cwd.child("json.json").write_str("{}\n ")?;
cwd.child("main.py").write_str(r#"print "abc" "#)?;
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: ["json"]
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types_or: ["json", "python"]
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
exclude_types: ["json"]
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1:]); exit(1)'
types: ["json" ]
exclude_types: ["json"]
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['json.json']
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['main.py', 'json.json']
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
['file.txt', '.pre-commit-config.yaml', 'main.py']
trailing-whitespace..................................(no files to check)Skipped
----- stderr -----
"#);
Ok(())
}
/// Abort the run if a hook fails.
#[test]
fn fail_fast() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Fixing files"); exit(1)'
always_run: true
fail_fast: false
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Fixing files"); exit(1)'
always_run: true
fail_fast: true
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -V
always_run: true
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -V
always_run: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
Fixing files
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
Fixing files
----- stderr -----
"#);
}
/// Test --fail-fast CLI flag stops execution after first failure.
#[test]
fn fail_fast_cli_flag() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: failing-hook
name: failing-hook
language: system
entry: python3 -c 'print("Failed"); exit(1)'
always_run: true
- id: passing-hook
name: passing-hook
language: system
entry: python3 -c 'print("Passed")'
always_run: true
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
failing-hook.............................................................Failed
- hook id: failing-hook
- exit code: 1
Failed
passing-hook.............................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--fail-fast"), @r#"
success: false
exit_code: 1
----- stdout -----
failing-hook.............................................................Failed
- hook id: failing-hook
- exit code: 1
Failed
----- stderr -----
"#);
}
/// Run from a subdirectory. File arguments should be fixed to be relative to the root.
#[test]
fn subdirectory() -> Result<()> {
let context = TestContext::new();
context.init_project();
let cwd = context.work_dir();
let child = cwd.child("foo/bar/baz");
child.create_dir_all()?;
child.child("file.txt").write_str("Hello, world!\n")?;
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sys.argv[1]); exit(1)'
always_run: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().current_dir(&child).arg("--files").arg("file.txt"), @r#"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
foo/bar/baz/file.txt
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--cd").arg(&*child).arg("--files").arg("file.txt"), @r#"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
foo/bar/baz/file.txt
----- stderr -----
"#);
Ok(())
}
/// Test hook `log_file` option.
#[test]
fn log_file() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print("Fixing files"); exit(1)'
always_run: true
log_file: log.txt
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
trailing-whitespace......................................................Failed
- hook id: trailing-whitespace
- exit code: 1
----- stderr -----
"#);
let log = context.read("log.txt");
assert_eq!(log, "Fixing files");
}
/// Pass pre-commit environment variables to the hook.
#[test]
fn pass_env_vars() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: env-vars
name: Pass environment
language: system
entry: python3 -c "import os, sys; print(os.getenv('PRE_COMMIT')); sys.exit(1)"
always_run: true
"#});
cmd_snapshot!(context.filters(), context.run(), @r###"
success: false
exit_code: 1
----- stdout -----
Pass environment.........................................................Failed
- hook id: env-vars
- exit code: 1
1
----- stderr -----
"###);
}
#[test]
fn staged_files_only() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'print(open("file.txt", "rt").read())'
verbose: true
types: [text]
"#});
context
.work_dir()
.child("file.txt")
.write_str("Hello, world!")?;
context.git_add(".");
// Non-staged files should be stashed and restored.
context
.work_dir()
.child("file.txt")
.write_str("Hello world again!")?;
let filters: Vec<_> = context
.filters()
.into_iter()
.chain([(r"/\d+-\d+.patch", "/[TIME]-[PID].patch")])
.collect();
cmd_snapshot!(filters, context.run(), @r"
success: true
exit_code: 0
----- stdout -----
trailing-whitespace......................................................Passed
- hook id: trailing-whitespace
- duration: [TIME]
Hello, world!
----- stderr -----
Unstaged changes detected, stashing unstaged changes to `[HOME]/patches/[TIME]-[PID].patch`
Restored working tree changes from `[HOME]/patches/[TIME]-[PID].patch`
");
let content = context.read("file.txt");
assert_snapshot!(content, @"Hello world again!");
Ok(())
}
#[cfg(unix)]
#[test]
fn restore_on_interrupt() -> Result<()> {
let context = TestContext::new();
context.init_project();
// The hook will sleep for 3 seconds.
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import time; open("out.txt", "wt").write(open("file.txt", "rt").read()); time.sleep(10)'
verbose: true
types: [text]
"#});
context
.work_dir()
.child("file.txt")
.write_str("Hello, world!")?;
context.git_add(".");
// Non-staged files should be stashed and restored.
context
.work_dir()
.child("file.txt")
.write_str("Hello world again!")?;
let mut child = context.run().spawn()?;
let child_id = child.id();
// Send an interrupt signal to the process.
let handle = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(1));
#[allow(clippy::cast_possible_wrap)]
unsafe {
libc::kill(child_id as i32, libc::SIGINT)
};
});
handle.join().unwrap();
child.wait()?;
let content = context.read("out.txt");
assert_snapshot!(content, @"Hello, world!");
let content = context.read("file.txt");
assert_snapshot!(content, @"Hello world again!");
Ok(())
}
/// When in merge conflict, runs on files that have conflicts fixed.
#[test]
fn merge_conflicts() -> Result<()> {
let context = TestContext::new();
context.init_project();
// Create a merge conflict.
let cwd = context.work_dir();
cwd.child("file.txt").write_str("Hello, world!")?;
context.git_add(".");
context.configure_git_author();
context.git_commit("Initial commit");
Command::new("git")
.arg("checkout")
.arg("-b")
.arg("feature")
.current_dir(cwd)
.assert()
.success();
cwd.child("file.txt").write_str("Hello, world again!")?;
context.git_add(".");
context.git_commit("Feature commit");
Command::new("git")
.arg("checkout")
.arg("master")
.current_dir(cwd)
.assert()
.success();
cwd.child("file.txt")
.write_str("Hello, world from master!")?;
context.git_add(".");
context.git_commit("Master commit");
Command::new("git")
.arg("merge")
.arg("feature")
.current_dir(cwd)
.assert()
.code(1);
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: trailing-whitespace
name: trailing-whitespace
language: system
entry: python3 -c 'import sys; print(sorted(sys.argv[1:]))'
verbose: true
"});
// Abort on merge conflicts.
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: You have unmerged paths. Resolve them before running prek
"#);
// Fix the conflict and run again.
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
trailing-whitespace......................................................Passed
- hook id: trailing-whitespace
- duration: [TIME]
['.pre-commit-config.yaml', 'file.txt']
----- stderr -----
"#);
Ok(())
}
/// Local python hook with no additional dependencies.
#[test]
fn local_python_hook() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: local-python-hook
name: local-python-hook
language: python
entry: python3 -c 'import sys; print("Hello, world!"); sys.exit(1)'
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
local-python-hook........................................................Failed
- hook id: local-python-hook
- exit code: 1
Hello, world!
----- stderr -----
"#);
}
/// Invalid `entry`
#[test]
fn invalid_entry() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: entry
name: entry
language: python
entry: '"'
"#});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 2
----- stdout -----
entry....................................................................
----- stderr -----
error: Failed to run hook `entry`
caused by: Hook `entry` is invalid
caused by: Failed to parse entry `"` as commands
"#);
}
/// Initialize a repo that does not exist.
#[test]
fn init_nonexistent_repo() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://notexistentatallnevergonnahappen.com/nonexistent/repo
rev: v1.0.0
hooks:
- id: nonexistent
name: nonexistent
"});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([(r"exit code: ", "exit status: "),
// Normalize Git error message to handle environment-specific variations
(
r"fatal: unable to access 'https://notexistentatallnevergonnahappen\.com/nonexistent/repo/':.*",
r"fatal: unable to access 'https://notexistentatallnevergonnahappen.com/nonexistent/repo/': [error]"
),
])
.collect::<Vec<_>>();
cmd_snapshot!(filters, context.run(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to initialize repo `https://notexistentatallnevergonnahappen.com/nonexistent/repo`
caused by: command `git full clone` exited with an error:
[status]
exit status: 128
[stderr]
fatal: unable to access 'https://notexistentatallnevergonnahappen.com/nonexistent/repo/': [error]
");
}
/// Test hooks that specifies `types: [directory]`.
#[test]
fn types_directory() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: directory
name: directory
language: system
entry: echo
types: [directory]
"});
context.work_dir().child("dir").create_dir_all()?;
context
.work_dir()
.child("dir/file.txt")
.write_str("Hello, world!")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--files").arg("dir"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r#"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--files").arg("non-exist-files"), @r#"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
warning: This file does not exist, it will be ignored: `non-exist-files`
"#);
Ok(())
}
#[test]
fn run_last_commit() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
let cwd = context.work_dir();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
"});
// Create initial files and make first commit
cwd.child("file1.txt").write_str("Hello, world!\n")?;
cwd.child("file2.txt")
.write_str("Initial content with trailing spaces \n")?; // This has issues but won't be in last commit
context.git_add(".");
context.git_commit("Initial commit");
// Modify files and make second commit with trailing whitespace
cwd.child("file1.txt").write_str("Hello, world! \n")?; // trailing whitespace
cwd.child("file3.txt").write_str("New file")?; // missing newline
// Note: file2.txt is NOT modified in this commit, so it should be filtered out by --last-commit
context.git_add(".");
context.git_commit("Second commit with issues");
// Run with --last-commit should only check files from the last commit
// This should only process file1.txt and file3.txt, NOT file2.txt
cmd_snapshot!(context.filters(), context.run().arg("--last-commit"), @r#"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing file1.txt
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing file3.txt
----- stderr -----
"#);
// Now reset the files to their problematic state for comparison
cwd.child("file1.txt").write_str("Hello, world! \n")?; // trailing whitespace
cwd.child("file3.txt").write_str("New file")?; // missing newline
// Run with --all-files should check ALL files including file2.txt
// This demonstrates that file2.txt was indeed filtered out in the previous test
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r#"
success: false
exit_code: 1
----- stdout -----
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing file1.txt
Fixing file2.txt
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing file3.txt
----- stderr -----
"#);
Ok(())
}
/// Test `prek run --files` with multiple files.
#[test]
fn run_multiple_files() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: multiple-files
name: multiple-files
language: system
entry: echo
verbose: true
types: [text]
"});
let cwd = context.work_dir();
cwd.child("file1.txt").write_str("Hello, world!")?;
cwd.child("file2.txt").write_str("Hello, world!")?;
context.git_add(".");
// `--files` with multiple files
cmd_snapshot!(context.filters(), context.run().arg("--files").arg("file1.txt").arg("file2.txt"), @r#"
success: true
exit_code: 0
----- stdout -----
multiple-files...........................................................Passed
- hook id: multiple-files
- duration: [TIME]
file2.txt file1.txt
----- stderr -----
"#);
Ok(())
}
/// Test `prek run --files` with no files.
#[test]
fn run_no_files() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: no-files
name: no-files
language: system
entry: echo
verbose: true
"});
context.git_add(".");
// `--files` with no files
cmd_snapshot!(context.filters(), context.run().arg("--files"), @r#"
success: true
exit_code: 0
----- stdout -----
no-files.................................................................Passed
- hook id: no-files
- duration: [TIME]
.pre-commit-config.yaml
----- stderr -----
"#);
}
/// Test `prek run --directory` flags.
#[test]
fn run_directory() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: directory
name: directory
language: system
entry: echo
verbose: true
"});
let cwd = context.work_dir();
cwd.child("dir1").create_dir_all()?;
cwd.child("dir1/file.txt").write_str("Hello, world!")?;
cwd.child("dir2").create_dir_all()?;
cwd.child("dir2/file.txt").write_str("Hello, world!")?;
context.git_add(".");
// one `--directory`
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
"#);
// repeated `--directory`
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--directory").arg("dir1"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
"#);
// multiple `--directory`
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--directory").arg("dir2"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir2/file.txt dir1/file.txt
----- stderr -----
"#);
// non-existing directory
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("non-existing-dir"), @r#"
success: true
exit_code: 0
----- stdout -----
directory............................................(no files to check)Skipped
----- stderr -----
"#);
// `--directory` with `--files`
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--files").arg("dir1/file.txt"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--directory").arg("dir1").arg("--files").arg("dir2/file.txt"), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir2/file.txt dir1/file.txt
----- stderr -----
"#);
// run `--directory` inside a subdirectory
cmd_snapshot!(context.filters(), context.run().current_dir(cwd.join("dir1")).arg("--directory").arg("."), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
"#);
cmd_snapshot!(context.filters(), context.run().arg("--cd").arg("dir1").arg("--directory").arg("."), @r#"
success: true
exit_code: 0
----- stdout -----
directory................................................................Passed
- hook id: directory
- duration: [TIME]
dir1/file.txt
----- stderr -----
"#);
Ok(())
}
/// Test `minimum_prek_version` option.
#[test]
fn minimum_prek_version() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
minimum_prek_version: 10.0.0
repos:
- repo: local
hooks:
- id: directory
name: directory
language: system
entry: echo
verbose: true
"});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([(
r"current version `\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?`",
"current version `[CURRENT_VERSION]`",
)])
.collect::<Vec<_>>();
cmd_snapshot!(filters, context.run(), @r#"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse `.pre-commit-config.yaml`
caused by: Required minimum prek version `10.0.0` is greater than current version `[CURRENT_VERSION]`. Please consider updating prek.
"#);
}
/// Run hooks that would echo color.
#[test]
#[cfg(not(windows))]
fn color() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: color
name: color
language: python
entry: python ./color.py
verbose: true
pass_filenames: false
"});
let script = indoc::indoc! {r"
import sys
if sys.stdout.isatty():
print('\033[1;32mHello, world!\033[0m')
else:
print('Hello, world!')
"};
context.work_dir().child("color.py").write_str(script)?;
context.git_add(".");
// Run default. In integration tests, we don't have a TTY.
// So this prints without color.
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
color....................................................................Passed
- hook id: color
- duration: [TIME]
Hello, world!
----- stderr -----
"#);
// Force color output
cmd_snapshot!(context.filters(), context.run().arg("--color=always"), @r#"
success: true
exit_code: 0
----- stdout -----
color....................................................................Passed
- hook id: color
- duration: [TIME]
 Hello, world!
----- stderr -----
"#);
Ok(())
}
/// Test running hook whose `entry` is script with shebang on Windows.
#[test]
fn shebang_script() -> Result<()> {
let context = TestContext::new();
context.init_project();
// Create a script with shebang.
let script = indoc::indoc! {r"
#!/usr/bin/env python
import sys
print('Hello, world!')
sys.exit(0)
"};
context.work_dir().child("script.py").write_str(script)?;
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: shebang-script
name: shebang-script
language: python
entry: script.py
verbose: true
pass_filenames: false
always_run: true
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
shebang-script...........................................................Passed
- hook id: shebang-script
- duration: [TIME]
Hello, world!
----- stderr -----
"#);
Ok(())
}
/// Test `git commit -a` works without `.git/index.lock exists` error.
#[test]
fn git_commit_a() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.disable_auto_crlf();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: echo
name: echo
language: system
entry: echo
verbose: true
"});
// Create a file and commit it.
let cwd = context.work_dir();
let file = cwd.child("file.txt");
file.write_str("Hello, world!\n")?;
cmd_snapshot!(context.filters(), context.install(), @r#"
success: true
exit_code: 0
----- stdout -----
prek installed at `.git/hooks/pre-commit`
----- stderr -----
"#);
context.git_add(".");
context.git_commit("Initial commit");
// Edit the file
file.write_str("Hello, world again!\n")?;
let mut commit = Command::new("git");
commit
.arg("commit")
.arg("-a")
.arg("-m")
.arg("Update file")
.current_dir(cwd);
let filters = context
.filters()
.into_iter()
.chain([(r"\[master \w{7}\]", r"[master COMMIT]")])
.collect::<Vec<_>>();
cmd_snapshot!(filters, commit, @r#"
success: true
exit_code: 0
----- stdout -----
[master COMMIT] Update file
1 file changed, 1 insertion(+), 1 deletion(-)
----- stderr -----
echo.....................................................................Passed
- hook id: echo
- duration: [TIME]
file.txt
"#);
Ok(())
}
fn write_pre_commit_config(path: &Path, hooks: &[(&str, &str)]) -> Result<()> {
let mut yaml = String::from(indoc::indoc! {"
repos:
- repo: local
hooks:
"});
for (id, name) in hooks {
let hook = textwrap::indent(
&indoc::formatdoc! {"
- id: {}
name: {}
entry: echo
language: system
", id, name
},
" ",
);
yaml.push_str(&hook);
}
std::fs::create_dir_all(path)?;
std::fs::write(path.join(CONFIG_FILE), yaml)?;
Ok(())
}
#[cfg(unix)]
#[test]
fn selectors_completion() -> Result<()> {
let context = TestContext::new();
let cwd = context.work_dir();
context.init_project();
// Root project with one hook
write_pre_commit_config(cwd, &[("root-hook", "Root Hook")])?;
// Nested project at app/ with one hook
let app = cwd.join("app");
write_pre_commit_config(&app, &[("app-hook", "App Hook")])?;
// Deeper nested project at app/lib/ with one hook
let app_lib = app.join("lib");
write_pre_commit_config(&app_lib, &[("lib-hook", "Lib Hook")])?;
// Unrelated non-project dir should not appear in subdir suggestions
cwd.child("scratch").create_dir_all()?;
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg(""), @r"
success: true
exit_code: 0
----- stdout -----
install Install the prek git hook
install-hooks Create hook environments for all hooks used in the config file
run Run hooks
list List available hooks
uninstall Uninstall the prek git hook
validate-config Validate `.pre-commit-config.yaml` files
validate-manifest Validate `.pre-commit-hooks.yaml` files
sample-config Produce a sample `.pre-commit-config.yaml` file
auto-update Auto-update pre-commit config to the latest repos' versions
cache Manage the prek cache
init-template-dir Install hook script in a directory intended for use with `git config init.templateDir`
try-repo Try the pre-commit hooks in the current repo
self `prek` self management
app/
app:
app-hook App Hook
lib-hook Lib Hook
root-hook Root Hook
--skip Skip the specified hooks or projects
--all-files Run on all files in the repo
--files Specific filenames to run hooks on
--directory Run hooks on all files in the specified directories
--from-ref The original ref in a `<from_ref>...<to_ref>` diff expression. Files changed in this diff will be run through the hooks
--to-ref The destination ref in a `from_ref...to_ref` diff expression. Defaults to `HEAD` if `from_ref` is specified
--last-commit Run hooks against the last commit. Equivalent to `--from-ref HEAD~1 --to-ref HEAD`
--hook-stage The stage during which the hook is fired
--show-diff-on-failure When hooks fail, run `git diff` directly afterward
--fail-fast Stop running hooks after the first failure
--dry-run Do not run the hooks, but print the hooks that would have been run
--config Path to alternate config file
--cd Change to directory before running
--color Whether to use color in output
--refresh Refresh all cached data
--help Display the concise help for this command
--no-progress Hide all progress outputs
--quiet Use quiet output
--verbose Use verbose output
--log-file Write trace logs to the specified file. If not specified, trace logs will be written to `$PREK_HOME/prek.log`
--version Display the prek version
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("ap"), @r"
success: true
exit_code: 0
----- stdout -----
app/
app:
app-hook App Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app:"), @r"
success: true
exit_code: 0
----- stdout -----
app:app-hook App Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app:app"), @r"
success: true
exit_code: 0
----- stdout -----
app:app-hook App Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib/
app/lib:
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/li"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib/
app/lib:
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/lib:"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib:lib-hook Lib Hook
----- stderr -----
");
cmd_snapshot!(context.filters(), context.run().env("COMPLETE", "fish").arg("--").arg("prek").arg("app/lib/"), @r"
success: true
exit_code: 0
----- stdout -----
app/lib/
----- stderr -----
");
Ok(())
}
/// Test reusing hook environments only when dependencies are exactly same. (ignore order)
#[test]
fn reuse_env() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/PyCQA/flake8
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: [flake8-errmsg]
"});
context
.work_dir()
.child("err.py")
.write_str("raise ValueError('error')\n")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
flake8...................................................................Failed
- hook id: flake8
- exit code: 1
err.py:1:1: EM101 Exceptions must not use a string literal; assign to a variable first
----- stderr -----
");
// Remove dependencies, so the environment should not be reused.
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/PyCQA/flake8
rev: 7.1.1
hooks:
- id: flake8
"});
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
flake8...................................................................Passed
----- stderr -----
");
// There should be two hook environments.
assert_eq!(context.home_dir().child("hooks").read_dir()?.count(), 2);
Ok(())
}
#[test]
fn dry_run() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: fail
name: fail
entry: fail
language: fail
"});
context.git_add(".");
// Run with `--dry-run`
cmd_snapshot!(context.filters(), context.run().arg("--dry-run").arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
fail.....................................................................Dry Run
- hook id: fail
- duration: [TIME]
`fail` would be run on 1 files:
- .pre-commit-config.yaml
----- stderr -----
");
}
/// Supports reading `pre-commit-config.yml` as well.
#[test]
fn alternate_config_file() -> Result<()> {
let context = TestContext::new();
context.init_project();
context
.work_dir()
.child(ALT_CONFIG_FILE)
.write_str(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: local-python-hook
name: local-python-hook
language: python
entry: python3 -c 'import sys; print("Hello, world!")'
"#})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
local-python-hook........................................................Passed
- hook id: local-python-hook
- duration: [TIME]
Hello, world!
----- stderr -----
");
context
.work_dir()
.child(CONFIG_FILE)
.write_str(indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: local-python-hook
name: local-python-hook
language: python
entry: python3 -c 'import sys; print("Hello, world!")'
"#})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run().arg("--refresh").arg("-v"), @r"
success: true
exit_code: 0
----- stdout -----
local-python-hook........................................................Passed
- hook id: local-python-hook
- duration: [TIME]
Hello, world!
----- stderr -----
warning: Both `[TEMP_DIR]/.pre-commit-config.yaml` and `[TEMP_DIR]/.pre-commit-config.yml` exist, using `[TEMP_DIR]/.pre-commit-config.yaml` only
");
Ok(())
}
#[test]
fn show_diff_on_failure() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.disable_auto_crlf();
let config = indoc::indoc! {r#"
repos:
- repo: local
hooks:
- id: modify
name: modify
language: python
entry: python -c "import sys; open('file.txt', 'a').write('Added line\n')"
pass_filenames: false
"#};
context.write_pre_commit_config(config);
context
.work_dir()
.child("file.txt")
.write_str("Original line\n")?;
context.git_add(".");
let mut filters = context.filters();
filters.push((r"index \w{7}\.\.\w{7} \d{6}", "index [OLD]..[NEW] 100644"));
// When failed in CI environment
cmd_snapshot!(filters.clone(), context.run().env(EnvVars::CI, "1").arg("--show-diff-on-failure").arg("-v"), @r"
success: false
exit_code: 1
----- stdout -----
modify...................................................................Failed
- hook id: modify
- duration: [TIME]
- files were modified by this hook
Hint: Some hooks made changes to the files.
If you are seeing this message in CI, reproduce locally with: `prek run --all-files`
To run prek as part of git workflow, use `prek install` to set up git hooks.
All changes made by hooks:
diff --git a/file.txt b/file.txt
index [OLD]..[NEW] 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
Original line
+Added line
----- stderr -----
");
context
.work_dir()
.child("file.txt")
.write_str("Original line\n")?;
context.git_add(".");
// When failed in non-CI environment
cmd_snapshot!(filters.clone(), context.run().env_remove(EnvVars::CI).arg("--show-diff-on-failure").arg("-v"), @r"
success: false
exit_code: 1
----- stdout -----
modify...................................................................Failed
- hook id: modify
- duration: [TIME]
- files were modified by this hook
All changes made by hooks:
diff --git a/file.txt b/file.txt
index [OLD]..[NEW] 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
Original line
+Added line
----- stderr -----
");
// Run in the `app` subproject.
let app = context.work_dir().child("app");
app.create_dir_all()?;
app.child("file.txt").write_str("Original line\n")?;
app.child(CONFIG_FILE).write_str(config)?;
Command::new("git")
.arg("add")
.arg(".")
.current_dir(&app)
.assert()
.success();
cmd_snapshot!(filters.clone(), context.run().env_remove(EnvVars::CI).current_dir(&app).arg("--show-diff-on-failure"), @r"
success: false
exit_code: 1
----- stdout -----
modify...................................................................Failed
- hook id: modify
- files were modified by this hook
All changes made by hooks:
diff --git a/app/file.txt b/app/file.txt
index [OLD]..[NEW] 100644
--- a/app/file.txt
+++ b/app/file.txt
@@ -1 +1,2 @@
Original line
+Added line
----- stderr -----
");
context.git_add(".");
// Run in the root
// Since we add a new subproject, use `--refresh` to find that.
cmd_snapshot!(filters.clone(), context.run().env_remove(EnvVars::CI).arg("--show-diff-on-failure").arg("--refresh"), @r"
success: false
exit_code: 1
----- stdout -----
Running hooks for `app`:
modify...................................................................Failed
- hook id: modify
- files were modified by this hook
Running hooks for `.`:
modify...................................................................Failed
- hook id: modify
- files were modified by this hook
All changes made by hooks:
diff --git a/app/file.txt b/app/file.txt
index [OLD]..[NEW] 100644
--- a/app/file.txt
+++ b/app/file.txt
@@ -1,2 +1,3 @@
Original line
Added line
+Added line
diff --git a/file.txt b/file.txt
index [OLD]..[NEW] 100644
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,3 @@
Original line
Added line
+Added line
----- stderr -----
");
Ok(())
}
#[test]
fn run_quiet() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: success
name: success
entry: echo
language: system
- id: fail
name: fail
entry: fail
language: fail
"});
context.git_add(".");
// Run with `--quiet`, only print failed hooks.
cmd_snapshot!(context.filters(), context.run().arg("--quiet"), @r"
success: false
exit_code: 1
----- stdout -----
fail.....................................................................Failed
- hook id: fail
- exit code: 1
fail
.pre-commit-config.yaml
----- stderr -----
");
// Run with `-qq`, do not print anything.
cmd_snapshot!(context.filters(), context.run().arg("-qq"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
");
}
/// Test `prek run --log-file <file>` flag.
#[test]
fn run_log_file() {
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: fail
name: fail
entry: fail
language: fail
"});
context.git_add(".");
// Run with `--no-log-file`, no `prek.log` is created.
cmd_snapshot!(context.filters(), context.run().arg("--no-log-file"), @r"
success: false
exit_code: 1
----- stdout -----
fail.....................................................................Failed
- hook id: fail
- exit code: 1
fail
.pre-commit-config.yaml
----- stderr -----
");
context
.home_dir()
.child("prek.log")
.assert(predicate::path::missing());
// Write log to `log`.
cmd_snapshot!(context.filters(), context.run().arg("--log-file").arg("log"), @r"
success: false
exit_code: 1
----- stdout -----
fail.....................................................................Failed
- hook id: fail
- exit code: 1
fail
.pre-commit-config.yaml
----- stderr -----
");
context
.work_dir()
.child("log")
.assert(predicate::path::exists());
}
/// Test `language_version: system` works and disables downloading.
#[test]
fn system_language_version() {
if !EnvVars::is_set(EnvVars::CI) {
// Skip when not running in CI, as we may not have toolchains installed locally.
return;
}
let context = TestContext::new();
context.init_project();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: local
hooks:
- id: system-node
name: system-node
language: node
language_version: system
entry: node -v
pass_filenames: false
- id: system-go
name: system-go
language: golang
language_version: system
entry: go version
pass_filenames: false
"});
context.git_add(".");
// Go and Node can't be found, `system` must fail.
cmd_snapshot!(
context.filters(),
context.run()
.arg("system-node")
.env(EnvVars::PREK_INTERNAL__GO_BINARY_NAME, "go-never-exist")
.env(EnvVars::PREK_INTERNAL__NODE_BINARY_NAME, "node-never-exist"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to install hook `system-node`
caused by: Failed to install node
caused by: No suitable system Node version found and downloads are disabled
");
cmd_snapshot!(
context.filters(),
context.run()
.arg("system-go")
.env(EnvVars::PREK_INTERNAL__GO_BINARY_NAME, "go-never-exist")
.env(EnvVars::PREK_INTERNAL__NODE_BINARY_NAME, "node-never-exist"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to install hook `system-go`
caused by: Failed to install go
caused by: No suitable system Go version found and downloads are disabled
");
// When Go and Node are available, hooks pass.
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
system-node..............................................................Passed
system-go................................................................Passed
----- stderr -----
");
}