1
0
mirror of https://github.com/j178/prek.git synced 2026-04-03 17:34:03 +02:00
Files
prek/tests/builtin_hooks.rs
Felix Blom aa6cd18cde Implement check-executables-have-shebangs as builtin-hook (#924)
* feat: Implement check-executables-have-shebangs hook

* Tweak

* Minor

* Tweak

* Minor

* feat: Implement check-executables-have-shebangs hook

* Fix paths

* Debug

* Debug

* Fix ls-files split

* Update docs

---------

Co-authored-by: Felix Blom <70511386+Felix-Blom@users.noreply.github.com>
Co-authored-by: Jo <10510431+j178@users.noreply.github.com>
2025-10-30 16:29:24 +08:00

1856 lines
55 KiB
Rust

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use anyhow::Result;
use assert_fs::prelude::*;
use constants::CONFIG_FILE;
use insta::assert_snapshot;
use crate::common::{TestContext, cmd_snapshot};
mod common;
#[test]
fn end_of_file_fixer_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
"});
let cwd = context.work_dir();
// Create test files
cwd.child("correct_lf.txt").write_str("Hello World\n")?;
cwd.child("correct_crlf.txt").write_str("Hello World\r\n")?;
cwd.child("no_newline.txt")
.write_str("No trailing newline")?;
cwd.child("multiple_lf.txt")
.write_str("Multiple newlines\n\n\n")?;
cwd.child("multiple_crlf.txt")
.write_str("Multiple newlines\r\n\r\n")?;
cwd.child("empty.txt").touch()?;
cwd.child("only_newlines.txt").write_str("\n\n")?;
cwd.child("only_win_newlines.txt").write_str("\r\n\r\n")?;
context.git_add(".");
// First run: hooks should fail and fix the files
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing multiple_crlf.txt
Fixing only_newlines.txt
Fixing only_win_newlines.txt
Fixing no_newline.txt
Fixing multiple_lf.txt
----- stderr -----
"#);
// Assert that the files have been corrected
assert_snapshot!(context.read("correct_lf.txt"), @"Hello World\n");
assert_snapshot!(context.read("correct_crlf.txt"), @"Hello World\n");
assert_snapshot!(context.read("no_newline.txt"), @"No trailing newline\n");
assert_snapshot!(context.read("multiple_lf.txt"), @"Multiple newlines\n");
assert_snapshot!(context.read("multiple_crlf.txt"), @"Multiple newlines\n");
assert_snapshot!(context.read("empty.txt"), @"");
assert_snapshot!(context.read("only_newlines.txt"), @"\n");
assert_snapshot!(context.read("only_win_newlines.txt"), @"\n");
context.git_add(".");
// Second run: hooks should now pass. The output will be stable.
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
fix end of files.........................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn check_yaml_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
"});
let cwd = context.work_dir();
// Create test files
cwd.child("valid.yaml").write_str("a: 1")?;
cwd.child("invalid.yaml").write_str("a: b: c")?;
cwd.child("duplicate.yaml").write_str("a: 1\na: 2")?;
cwd.child("empty.yaml").touch()?;
context.git_add(".");
// First run: hooks should fail
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1
duplicate.yaml: Failed to yaml decode (duplicate entry with key "a")
invalid.yaml: Failed to yaml decode (mapping values are not allowed in this context at line 1 column 5)
----- stderr -----
"#);
// Fix the files
cwd.child("invalid.yaml").write_str("a:\n b: c")?;
cwd.child("duplicate.yaml").write_str("a: 1\nb: 2")?;
context.git_add(".");
// Second run: hooks should now pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check yaml...............................................................Passed
----- stderr -----
"#);
Ok(())
}
/// `--allow-multiple-documents` feature is not implemented in Rust,
/// it should work by delegating to the original Python implementation.
#[test]
fn check_yaml_multiple_document() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
name: Python version
args: [ --allow-multiple-documents ]
- id: check-yaml
name: Rust version
"});
context
.work_dir()
.child("multiple.yaml")
.write_str(indoc::indoc! {r"
---
a: 1
---
b: 2
"
})?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
Python version...........................................................Passed
Rust version.............................................................Failed
- hook id: check-yaml
- exit code: 1
multiple.yaml: Failed to yaml decode (deserializing from YAML containing more than one document is not supported)
----- stderr -----
"#);
Ok(())
}
#[test]
fn check_json_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-json
"});
let cwd = context.work_dir();
// Create test files
cwd.child("valid.json").write_str(r#"{"a": 1}"#)?;
cwd.child("invalid.json").write_str(r#"{"a": 1,}"#)?;
cwd.child("duplicate.json")
.write_str(r#"{"a": 1, "a": 2}"#)?;
cwd.child("empty.json").touch()?;
context.git_add(".");
// First run: hooks should fail
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
check json...............................................................Failed
- hook id: check-json
- exit code: 1
duplicate.json: Failed to json decode (duplicate key `a` at line 1 column 12)
invalid.json: Failed to json decode (trailing comma at line 1 column 9)
----- stderr -----
");
// Fix the files
cwd.child("invalid.json").write_str(r#"{"a": 1}"#)?;
cwd.child("duplicate.json")
.write_str(r#"{"a": 1, "b": 2}"#)?;
context.git_add(".");
// Second run: hooks should now pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check json...............................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn mixed_line_ending_hook() -> 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: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: mixed-line-ending
"});
let cwd = context.work_dir();
// Create test files
cwd.child("mixed.txt")
.write_str("line1\nline2\r\nline3\r\n")?;
cwd.child("only_lf.txt").write_str("line1\nline2\n")?;
cwd.child("only_crlf.txt").write_str("line1\r\nline2\r\n")?;
cwd.child("no_endings.txt").write_str("hello world")?;
cwd.child("empty.txt").touch()?;
context.git_add(".");
// First run: hooks should fail and fix the files
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
mixed line ending........................................................Failed
- hook id: mixed-line-ending
- exit code: 1
- files were modified by this hook
Fixing mixed.txt
----- stderr -----
");
// Assert that the files have been corrected
assert_snapshot!(context.read("mixed.txt"), @"line1\r\nline2\r\nline3\r\n");
assert_snapshot!(context.read("only_lf.txt"), @"line1\nline2\n");
assert_snapshot!(context.read("only_crlf.txt"), @"line1\r\nline2\r\n");
context.git_add(".");
// Second run: hooks should now pass.
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
mixed line ending........................................................Passed
----- stderr -----
"#);
// Test with --fix=no
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: mixed-line-ending
args: ['--fix=no']
"});
context
.work_dir()
.child("mixed.txt")
.write_str("line1\nline2\r\n")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
mixed line ending........................................................Failed
- hook id: mixed-line-ending
- exit code: 1
mixed.txt: mixed line endings
----- stderr -----
"#);
assert_snapshot!(context.read("mixed.txt"), @"line1\nline2\r\n");
// Test with --fix=crlf
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: mixed-line-ending
args: ['--fix', 'crlf']
"});
context
.work_dir()
.child("mixed.txt")
.write_str("line1\nline2\r\n")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
mixed line ending........................................................Failed
- hook id: mixed-line-ending
- exit code: 1
- files were modified by this hook
Fixing .pre-commit-config.yaml
Fixing mixed.txt
Fixing only_lf.txt
----- stderr -----
");
assert_snapshot!(context.read("mixed.txt"), @"line1\r\nline2\r\n");
// Test mixed args with missing value for `--fix`
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: mixed-line-ending
args: ['--fix']
"});
context
.work_dir()
.child("mixed.txt")
.write_str("line1\nline2\r\nline3\n")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 2
----- stdout -----
mixed line ending........................................................
----- stderr -----
error: Failed to run hook `mixed-line-ending`
caused by: error: a value is required for '--fix <FIX>' but none was supplied
[possible values: auto, no, lf, crlf, cr]
");
Ok(())
}
#[test]
fn check_added_large_files_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
// Create an initial commit
let cwd = context.work_dir();
cwd.child("README.md").write_str("Initial commit")?;
context.git_add(".");
context.git_commit("Initial commit");
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: ['--maxkb', '1']
"});
// Create test files
cwd.child("small_file.txt").write_str("Hello World\n")?;
let large_file = cwd.child("large_file.txt");
large_file.write_binary(&[0; 2048])?; // 2KB file
context.git_add(".");
// First run: hook should fail because of the large file
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check for added large files..............................................Failed
- hook id: check-added-large-files
- exit code: 1
large_file.txt (2 KB) exceeds 1 KB
----- stderr -----
"#);
// Commit the files
context.git_add(".");
context.git_commit("Add large file");
// Create a new unstaged large file
let unstaged_large_file = cwd.child("unstaged_large_file.txt");
unstaged_large_file.write_binary(&[0; 2048])?; // 2KB file
context.git_add("unstaged_large_file.txt");
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: ['--maxkb=1', '--enforce-all']
"});
// Second run: the hook should check all files even if not staged
cmd_snapshot!(context.filters(), context.run().arg("--all-files"), @r#"
success: false
exit_code: 1
----- stdout -----
check for added large files..............................................Failed
- hook id: check-added-large-files
- exit code: 1
unstaged_large_file.txt (2 KB) exceeds 1 KB
large_file.txt (2 KB) exceeds 1 KB
----- stderr -----
"#);
context.git_rm("unstaged_large_file.txt");
context.git_clean();
// Test git-lfs integration
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: ['--maxkb=1']
"});
cwd.child(".gitattributes")
.write_str("*.dat filter=lfs diff=lfs merge=lfs -text")?;
context.git_add(".gitattributes");
let lfs_file = cwd.child("lfs_file.dat");
lfs_file.write_binary(&[0; 2048])?; // 2KB file
context.git_add(".");
// Third run: hook should pass because the large file is tracked by git-lfs
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check for added large files..............................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn builtin_hooks_workspace_mode() -> 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: meta
hooks:
- id: identity
"});
// Subproject with built-in hooks.
let app = context.work_dir().child("app");
app.create_dir_all()?;
app.child(CONFIG_FILE).write_str(indoc::indoc! {r"
repos:
- repo: meta
hooks:
- id: identity
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: mixed-line-ending
- id: trailing-whitespace
- id: check-added-large-files
args: ['--maxkb', '1']
"})?;
app.child("eof_no_newline.txt")
.write_str("No trailing newline")?;
app.child("eof_multiple_lf.txt").write_str("Multiple\n\n")?;
app.child("mixed.txt").write_str("line1\nline2\r\n")?;
app.child("trailing_ws.txt")
.write_str("line with trailing space \n")?;
app.child("correct.txt").write_str("All good here\n")?;
app.child("invalid.yaml").write_str("a: b: c")?;
app.child("duplicate.yaml").write_str("a: 1\na: 2")?;
app.child("empty.yaml").touch()?;
app.child("invalid.json").write_str(r#"{"a": 1,}"#)?;
app.child("duplicate.json")
.write_str(r#"{"a": 1, "a": 2}"#)?;
app.child("empty.json").touch()?;
// 2KB file to trigger check-added-large-files (1 KB threshold).
app.child("large.bin").write_binary(&[0u8; 2048])?;
context.git_add(".");
// First run: expect failures and auto-fixes where applicable.
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
Running hooks for `app`:
identity.................................................................Passed
- hook id: identity
- duration: [TIME]
correct.txt
invalid.yaml
empty.json
duplicate.json
trailing_ws.txt
large.bin
eof_multiple_lf.txt
duplicate.yaml
empty.yaml
mixed.txt
invalid.json
.pre-commit-config.yaml
eof_no_newline.txt
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing invalid.yaml
Fixing duplicate.json
Fixing eof_no_newline.txt
Fixing eof_multiple_lf.txt
Fixing duplicate.yaml
Fixing invalid.json
check yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1
duplicate.yaml: Failed to yaml decode (duplicate entry with key "a")
invalid.yaml: Failed to yaml decode (mapping values are not allowed in this context at line 1 column 5)
check json...............................................................Failed
- hook id: check-json
- exit code: 1
duplicate.json: Failed to json decode (duplicate key `a` at line 1 column 12)
invalid.json: Failed to json decode (trailing comma at line 1 column 9)
mixed line ending........................................................Failed
- hook id: mixed-line-ending
- exit code: 1
- files were modified by this hook
Fixing mixed.txt
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook
Fixing trailing_ws.txt
check for added large files..............................................Passed
Running hooks for `.`:
identity.................................................................Passed
- hook id: identity
- duration: [TIME]
app/.pre-commit-config.yaml
app/invalid.json
app/duplicate.yaml
app/correct.txt
app/mixed.txt
app/invalid.yaml
app/empty.yaml
app/duplicate.json
app/empty.json
app/large.bin
app/eof_no_newline.txt
.pre-commit-config.yaml
app/eof_multiple_lf.txt
app/trailing_ws.txt
----- stderr -----
"#);
// Fix YAML and JSON issues, then stage.
app.child("invalid.yaml").write_str("a:\n b: c")?;
app.child("duplicate.yaml").write_str("a: 1\nb: 2")?;
app.child("invalid.json").write_str(r#"{"a": 1}"#)?;
app.child("duplicate.json")
.write_str(r#"{"a": 1, "b": 2}"#)?;
context.git_add(".");
// Second run: all hooks should pass.
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
Running hooks for `app`:
identity.................................................................Passed
- hook id: identity
- duration: [TIME]
correct.txt
invalid.yaml
empty.json
duplicate.json
trailing_ws.txt
large.bin
eof_multiple_lf.txt
duplicate.yaml
empty.yaml
mixed.txt
invalid.json
.pre-commit-config.yaml
eof_no_newline.txt
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing invalid.yaml
Fixing duplicate.json
Fixing duplicate.yaml
Fixing invalid.json
check yaml...............................................................Passed
check json...............................................................Passed
mixed line ending........................................................Passed
trim trailing whitespace.................................................Passed
check for added large files..............................................Passed
Running hooks for `.`:
identity.................................................................Passed
- hook id: identity
- duration: [TIME]
app/.pre-commit-config.yaml
app/invalid.json
app/duplicate.yaml
app/correct.txt
app/mixed.txt
app/invalid.yaml
app/empty.yaml
app/duplicate.json
app/empty.json
app/large.bin
app/eof_no_newline.txt
.pre-commit-config.yaml
app/eof_multiple_lf.txt
app/trailing_ws.txt
----- stderr -----
");
Ok(())
}
#[test]
fn fix_byte_order_marker_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: fix-byte-order-marker
"});
let cwd = context.work_dir();
// Create test files
cwd.child("without_bom.txt").write_str("Hello, World!")?;
cwd.child("with_bom.txt").write_binary(&[
0xef, 0xbb, 0xbf, b'H', b'e', b'l', b'l', b'o', b',', b' ', b'W', b'o', b'r', b'l', b'd',
b'!',
])?;
cwd.child("bom_only.txt")
.write_binary(&[0xef, 0xbb, 0xbf])?;
cwd.child("empty.txt").touch()?;
context.git_add(".");
// First run: hooks should fix files with BOM
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
fix utf-8 byte order marker..............................................Failed
- hook id: fix-byte-order-marker
- exit code: 1
- files were modified by this hook
bom_only.txt: removed byte-order marker
with_bom.txt: removed byte-order marker
----- stderr -----
");
// Verify the content is correct
assert_eq!(context.read("with_bom.txt"), "Hello, World!");
assert_eq!(context.read("bom_only.txt"), "");
assert_eq!(context.read("without_bom.txt"), "Hello, World!");
assert_eq!(context.read("empty.txt"), "");
context.git_add(".");
// Second run: all should pass now
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
fix utf-8 byte order marker..............................................Passed
----- stderr -----
");
Ok(())
}
#[test]
#[cfg(unix)]
fn check_symlinks_hook_unix() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-symlinks
"});
let cwd = context.work_dir();
// Create test files
cwd.child("regular.txt").write_str("regular file")?;
cwd.child("target.txt").write_str("target content")?;
// Create valid symlink
std::os::unix::fs::symlink(
cwd.child("target.txt").path(),
cwd.child("valid_link.txt").path(),
)?;
// Create broken symlink
std::os::unix::fs::symlink(
cwd.child("nonexistent.txt").path(),
cwd.child("broken_link.txt").path(),
)?;
context.git_add(".");
// First run: should fail due to broken symlink
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check for broken symlinks................................................Failed
- hook id: check-symlinks
- exit code: 1
broken_link.txt: Broken symlink
----- stderr -----
"#);
// Remove broken symlink
std::fs::remove_file(cwd.child("broken_link.txt").path())?;
context.git_add(".");
// Second run: should pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check for broken symlinks................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
#[cfg(windows)]
fn check_symlinks_hook_windows() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-symlinks
"});
let cwd = context.work_dir();
// Create test files
cwd.child("regular.txt").write_str("regular file")?;
cwd.child("target.txt").write_str("target content")?;
// Try to create valid symlink (may fail without admin/developer mode)
let valid_link_result = std::os::windows::fs::symlink_file(
cwd.child("target.txt").path(),
cwd.child("valid_link.txt").path(),
);
// Try to create broken symlink (may fail without admin/developer mode)
let broken_link_result = std::os::windows::fs::symlink_file(
cwd.child("nonexistent.txt").path(),
cwd.child("broken_link.txt").path(),
);
// Skip test if we can't create symlinks (insufficient permissions)
if valid_link_result.is_err() || broken_link_result.is_err() {
// Skipping test: insufficient permissions for symlink creation on Windows
return Ok(());
}
context.git_add(".");
// First run: should fail due to broken symlink
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check for broken symlinks................................................Failed
- hook id: check-symlinks
- exit code: 1
broken_link.txt: Broken symlink
----- stderr -----
"#);
// Remove broken symlink
std::fs::remove_file(cwd.child("broken_link.txt").path())?;
context.git_add(".");
// Second run: should pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check for broken symlinks................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn detect_private_key_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
"});
let cwd = context.work_dir();
// Create test files - various private key types
cwd.child("id_rsa")
.write_str("-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----\n")?;
cwd.child("id_dsa")
.write_str("-----BEGIN DSA PRIVATE KEY-----\nAAAAA...\n-----END DSA PRIVATE KEY-----\n")?;
cwd.child("id_ecdsa")
.write_str("-----BEGIN EC PRIVATE KEY-----\nMHc...\n-----END EC PRIVATE KEY-----\n")?;
cwd.child("id_ed25519").write_str(
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNz...\n-----END OPENSSH PRIVATE KEY-----\n",
)?;
cwd.child("key.ppk")
.write_str("PuTTY-User-Key-File-2: ssh-rsa\nEncryption: none\n")?;
cwd.child("private.asc")
.write_str("-----BEGIN PGP PRIVATE KEY BLOCK-----\nVersion: GnuPG...\n")?;
cwd.child("ta.key").write_str(
"#\n# 2048 bit OpenVPN static key\n#\n-----BEGIN OpenVPN Static key V1-----\n",
)?;
cwd.child("doc.txt").write_str(
"Some documentation\n\nHere is a key:\n-----BEGIN RSA PRIVATE KEY-----\ndata\n",
)?;
cwd.child("safe1.txt")
.write_str("This file talks about BEGIN_RSA_PRIVATE_KEY but doesn't contain one\n")?;
cwd.child("safe2.txt")
.write_str("This is just a regular file\nwith some content\n")?;
cwd.child("empty.txt").touch()?;
context.git_add(".");
// First run: hooks should fail due to private keys
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
detect private key.......................................................Failed
- hook id: detect-private-key
- exit code: 1
Private key found: doc.txt
Private key found: id_ecdsa
Private key found: key.ppk
Private key found: id_rsa
Private key found: id_dsa
Private key found: id_ed25519
Private key found: ta.key
Private key found: private.asc
----- stderr -----
");
// Remove all private keys
context.git_rm("id_rsa");
context.git_rm("id_dsa");
context.git_rm("id_ecdsa");
context.git_rm("id_ed25519");
context.git_rm("key.ppk");
context.git_rm("private.asc");
context.git_rm("ta.key");
context.git_rm("doc.txt");
context.git_clean();
context.git_add(".");
// Second run: hooks should now pass
cmd_snapshot!(context.filters(), context.run(), @r"
success: true
exit_code: 0
----- stdout -----
detect private key.......................................................Passed
----- stderr -----
");
Ok(())
}
#[test]
fn check_merge_conflict_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-merge-conflict
args: ['--assume-in-merge']
"});
let cwd = context.work_dir();
// Create test files with conflict markers
cwd.child("conflict.txt").write_str(indoc::indoc! {r"
Before conflict
<<<<<<< HEAD
Our changes
=======
Their changes
>>>>>>> branch
After conflict
"})?;
cwd.child("clean.txt").write_str("No conflicts here\n")?;
cwd.child("partial_conflict.txt")
.write_str(indoc::indoc! {r"
Some content
<<<<<<< HEAD
Conflicting line
"})?;
context.git_add(".");
// First run: hooks should fail due to conflict markers
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check for merge conflicts................................................Failed
- hook id: check-merge-conflict
- exit code: 1
partial_conflict.txt:2: Merge conflict string "<<<<<<< " found
conflict.txt:2: Merge conflict string "<<<<<<< " found
conflict.txt:4: Merge conflict string "=======" found
conflict.txt:6: Merge conflict string ">>>>>>> " found
----- stderr -----
"#);
// Fix the files by removing conflict markers
cwd.child("conflict.txt").write_str(indoc::indoc! {r"
Before conflict
Our changes
After conflict
"})?;
cwd.child("partial_conflict.txt")
.write_str("Some content\nResolved line\n")?;
context.git_add(".");
// Second run: hooks should now pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check for merge conflicts................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn check_merge_conflict_without_assume_flag() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
// Without --assume-in-merge, hook should pass even with conflict markers
// if we're not actually in a merge state
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-merge-conflict
"});
let cwd = context.work_dir();
cwd.child("conflict.txt").write_str(indoc::indoc! {r"
<<<<<<< HEAD
Our changes
=======
Their changes
>>>>>>> branch
"})?;
context.git_add(".");
// Should pass because we're not in a merge state and no --assume-in-merge flag
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check for merge conflicts................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn check_xml_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-xml
"});
let cwd = context.work_dir();
// Create test files
cwd.child("valid.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element>value</element>
</root>"#,
)?;
cwd.child("invalid_unclosed.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element>value
</root>"#,
)?;
cwd.child("invalid_mismatched.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element>value</different>
</root>"#,
)?;
cwd.child("multiple_roots.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<element>value</element>
<another>value</another>"#,
)?;
cwd.child("empty.xml").touch()?;
context.git_add(".");
// First run: hooks should fail
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
check xml................................................................Failed
- hook id: check-xml
- exit code: 1
invalid_mismatched.xml: Failed to xml parse (ill-formed document: expected `</element>`, but `</different>` was found)
empty.xml: Failed to xml parse (no element found)
invalid_unclosed.xml: Failed to xml parse (ill-formed document: expected `</element>`, but `</root>` was found)
multiple_roots.xml: Failed to xml parse (junk after document element)
----- stderr -----
");
// Fix the files
cwd.child("invalid_unclosed.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element>value</element>
</root>"#,
)?;
cwd.child("invalid_mismatched.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element>value</element>
</root>"#,
)?;
cwd.child("multiple_roots.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element>value</element>
<another>value</another>
</root>"#,
)?;
context.git_add(".");
// Second run: hooks should now pass
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 1
----- stdout -----
check xml................................................................Failed
- hook id: check-xml
- exit code: 1
empty.xml: Failed to xml parse (no element found)
----- stderr -----
");
Ok(())
}
#[test]
fn check_xml_with_features() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-xml
"});
let cwd = context.work_dir();
// Create test files with various XML features
cwd.child("with_attributes.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="http://example.com">
<element id="1" type="test">value</element>
</root>"#,
)?;
cwd.child("with_cdata.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<element><![CDATA[Some <special> characters & symbols]]></element>
</root>"#,
)?;
cwd.child("with_comments.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<root>
<!-- This is a comment -->
<element>value</element>
</root>"#,
)?;
cwd.child("with_doctype.xml").write_str(
r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root SYSTEM "root.dtd">
<root>
<element>value</element>
</root>"#,
)?;
context.git_add(".");
// All should pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check xml................................................................Passed
----- stderr -----
"#);
Ok(())
}
#[test]
fn no_commit_to_branch_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: no-commit-to-branch
"});
let cwd = context.work_dir();
// Create a test file
cwd.child("test.txt").write_str("Hello World")?;
context.git_add(".");
context.git_commit("Initial commit");
// Test 1: Try to commit to master branch (should fail)
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
don't commit to branch...................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
You are not allowed to commit to branch 'master'
----- stderr -----
"#);
// Test 2: Create and switch to a feature branch (should pass)
context.git_branch("feature/new-feature");
context.git_checkout("feature/new-feature");
cwd.child("feature.txt").write_str("Feature content")?;
context.git_add(".");
context.git_commit("Add feature");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
don't commit to branch...................................................Passed
----- stderr -----
"#);
// Test 3: Try to commit to main branch (should fail)
context.git_branch("main");
context.git_checkout("main");
cwd.child("main.txt").write_str("Main content")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
don't commit to branch...................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
You are not allowed to commit to branch 'main'
----- stderr -----
"#);
Ok(())
}
#[test]
fn no_commit_to_branch_hook_with_custom_branches() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: no-commit-to-branch
args: ['--branch', 'develop', '--branch', 'production']
"});
let cwd = context.work_dir();
// Create a test file
cwd.child("test.txt").write_str("Hello World")?;
context.git_add(".");
context.git_commit("Initial commit");
// Test 1: Try to commit to master branch (should pass - not in custom list)
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
don't commit to branch...................................................Passed
----- stderr -----
"#);
// Test 2: Create and switch to develop branch (should fail)
context.git_branch("develop");
context.git_checkout("develop");
cwd.child("develop.txt").write_str("Develop content")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
don't commit to branch...................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
You are not allowed to commit to branch 'develop'
----- stderr -----
"#);
// Test 3: Create and switch to production branch (should fail)
context.git_branch("production");
context.git_checkout("production");
cwd.child("production.txt")
.write_str("Production content")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
don't commit to branch...................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
You are not allowed to commit to branch 'production'
----- stderr -----
"#);
Ok(())
}
#[test]
fn no_commit_to_branch_hook_with_patterns() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: no-commit-to-branch
args: ['--pattern', '^feature/.*', '--pattern', '.*-wip$']
"});
let cwd = context.work_dir();
// Create a test file
cwd.child("test.txt").write_str("Hello World")?;
context.git_add(".");
context.git_commit("Initial commit");
// Test 1: Try to commit to master branch (should fail - If branch is not specified, branch defaults to master and main)
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
don't commit to branch...................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
You are not allowed to commit to branch 'master'
----- stderr -----
"#);
// Test 2: Create and switch to feature branch (should fail - matches pattern)
context.git_branch("feature/new-feature");
context.git_checkout("feature/new-feature");
cwd.child("feature.txt").write_str("Feature content")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
don't commit to branch...................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
You are not allowed to commit to branch 'feature/new-feature'
----- stderr -----
"#);
// Test 3: Create and switch to wip branch (should fail - matches pattern)
context.git_branch("my-branch-wip");
context.git_checkout("my-branch-wip");
cwd.child("wip.txt").write_str("WIP content")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
don't commit to branch...................................................Failed
- hook id: no-commit-to-branch
- exit code: 1
You are not allowed to commit to branch 'my-branch-wip'
----- stderr -----
"#);
// Test 4: Create and switch to normal branch (should pass - doesn't match patterns)
context.git_branch("normal-branch");
context.git_checkout("normal-branch");
cwd.child("normal.txt").write_str("Normal content")?;
context.git_add(".");
context.git_commit("Add normal content");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
don't commit to branch...................................................Passed
----- stderr -----
"#);
// Test 5: Try to run with detached head pointer status (should pass - ignore this status)
context.git_checkout("HEAD~1");
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
don't commit to branch...................................................Passed
----- stderr -----
"#);
// Test 6: Try to commit to branch with invalid pattern (should fail - invalid pattern)
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: no-commit-to-branch
args: ['--pattern', '*invalid-pattern*']
"});
context.git_branch("invalid-branch");
context.git_checkout("invalid-branch");
cwd.child("invalid.txt").write_str("Invalid content")?;
context.git_add(".");
cmd_snapshot!(context.filters(), context.run(), @r"
success: false
exit_code: 2
----- stdout -----
don't commit to branch...................................................
----- stderr -----
error: Failed to run hook `no-commit-to-branch`
caused by: Failed to compile regex patterns
caused by: Parsing error at position 0: Target of repeat operator is invalid
");
Ok(())
}
#[cfg(unix)]
#[test]
fn check_executables_have_shebangs_hook() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-executables-have-shebangs
"});
let cwd = context.work_dir();
// Create test files
cwd.child("script_with_shebang.sh")
.write_str("#!/bin/bash\necho ok\n")?;
cwd.child("script_without_shebang.sh")
.write_str("echo missing shebang\n")?;
cwd.child("not_executable.txt")
.write_str("not executable\n")?;
cwd.child("empty.sh").touch()?;
// Mark scripts as executable
std::fs::set_permissions(
cwd.child("script_with_shebang.sh").path(),
std::fs::Permissions::from_mode(0o755),
)?;
std::fs::set_permissions(
cwd.child("script_without_shebang.sh").path(),
std::fs::Permissions::from_mode(0o755),
)?;
std::fs::set_permissions(
cwd.child("empty.sh").path(),
std::fs::Permissions::from_mode(0o755),
)?;
context.git_add(".");
// First run: should fail for script_without_shebang.sh and empty.sh
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check that executables have shebangs.....................................Failed
- hook id: check-executables-have-shebangs
- exit code: 1
empty.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x empty.sh'
If on Windows, you may also need to: 'git add --chmod=-x empty.sh'
If it is supposed to be executable, double-check its shebang.
script_without_shebang.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x script_without_shebang.sh'
If on Windows, you may also need to: 'git add --chmod=-x script_without_shebang.sh'
If it is supposed to be executable, double-check its shebang.
----- stderr -----
"#);
// Fix the files: remove executable bit or add shebang
cwd.child("script_without_shebang.sh")
.write_str("#!/bin/sh\necho fixed\n")?;
std::fs::set_permissions(
cwd.child("empty.sh").path(),
std::fs::Permissions::from_mode(0o644),
)?;
context.git_add(".");
// Second run: should now pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check that executables have shebangs.....................................Passed
----- stderr -----
"#);
Ok(())
}
#[cfg(windows)]
#[test]
fn check_executables_have_shebangs_win() -> Result<()> {
use std::process::Command;
let context = TestContext::new();
context.init_project();
context.configure_git_author();
let repo_path = 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: check-executables-have-shebangs
"});
let cwd = context.work_dir();
cwd.child("win_script_with_shebang.sh")
.write_str("#!/bin/bash\necho ok\n")?;
cwd.child("win_script_without_shebang.sh")
.write_str("missing shebang\n")?;
context.git_add(".");
Command::new("git")
.args(["update-index", "--chmod=+x", "win_script_with_shebang.sh"])
.current_dir(repo_path)
.status()?;
Command::new("git")
.args([
"update-index",
"--chmod=+x",
"win_script_without_shebang.sh",
])
.current_dir(repo_path)
.status()?;
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check that executables have shebangs.....................................Failed
- hook id: check-executables-have-shebangs
- exit code: 1
win_script_without_shebang.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x win_script_without_shebang.sh'
If on Windows, you may also need to: 'git add --chmod=-x win_script_without_shebang.sh'
If it is supposed to be executable, double-check its shebang.
----- stderr -----
"#);
Ok(())
}
#[cfg(unix)]
#[test]
fn check_executables_have_shebangs_various_cases() -> Result<()> {
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-executables-have-shebangs
"});
let cwd = context.work_dir();
// Create test files
cwd.child("partial_shebang.sh")
.write_str("#\necho partial\n")?;
cwd.child("shebang_with_space.sh")
.write_str("#! /bin/bash\necho ok\n")?;
cwd.child("non_executable.txt")
.write_str("not executable\n")?;
cwd.child("whitespace.sh").write_str(" \n")?;
cwd.child("invalid_shebang.sh")
.write_str("##!/bin/bash\necho bad\n")?;
// Mark scripts as executable
std::fs::set_permissions(
cwd.child("partial_shebang.sh").path(),
std::fs::Permissions::from_mode(0o755),
)?;
std::fs::set_permissions(
cwd.child("shebang_with_space.sh").path(),
std::fs::Permissions::from_mode(0o755),
)?;
std::fs::set_permissions(
cwd.child("whitespace.sh").path(),
std::fs::Permissions::from_mode(0o755),
)?;
std::fs::set_permissions(
cwd.child("invalid_shebang.sh").path(),
std::fs::Permissions::from_mode(0o755),
)?;
// non_executable.txt is not marked executable
context.git_add(".");
// Run: should fail for partial_shebang.sh, whitespace.sh, invalid_shebang.sh
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check that executables have shebangs.....................................Failed
- hook id: check-executables-have-shebangs
- exit code: 1
partial_shebang.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x partial_shebang.sh'
If on Windows, you may also need to: 'git add --chmod=-x partial_shebang.sh'
If it is supposed to be executable, double-check its shebang.
invalid_shebang.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x invalid_shebang.sh'
If on Windows, you may also need to: 'git add --chmod=-x invalid_shebang.sh'
If it is supposed to be executable, double-check its shebang.
whitespace.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x whitespace.sh'
If on Windows, you may also need to: 'git add --chmod=-x whitespace.sh'
If it is supposed to be executable, double-check its shebang.
----- stderr -----
"#);
// Fix the files: add valid shebangs or remove executable bit
cwd.child("partial_shebang.sh")
.write_str("#!/bin/sh\necho fixed\n")?;
cwd.child("whitespace.sh").write_str("#!/bin/sh\n")?;
cwd.child("invalid_shebang.sh")
.write_str("#!/bin/bash\necho fixed\n")?;
context.git_add(".");
// Second run: should now pass
cmd_snapshot!(context.filters(), context.run(), @r#"
success: true
exit_code: 0
----- stdout -----
check that executables have shebangs.....................................Passed
----- stderr -----
"#);
Ok(())
}
#[cfg(windows)]
#[test]
fn check_executables_have_shebangs_various_cases_win() -> Result<()> {
use std::process::Command;
let context = TestContext::new();
context.init_project();
context.configure_git_author();
context.write_pre_commit_config(indoc::indoc! {r"
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-executables-have-shebangs
"});
let cwd = context.work_dir();
cwd.child("partial_shebang.sh")
.write_str("#\necho partial\n")?;
cwd.child("shebang_with_space.sh")
.write_str("#! /bin/bash\necho ok\n")?;
cwd.child("non_executable.txt")
.write_str("not executable\n")?;
cwd.child("whitespace.sh").write_str(" \n")?;
cwd.child("invalid_shebang.sh")
.write_str("##!/bin/bash\necho bad\n")?;
context.git_add(".");
let executable_files = [
"partial_shebang.sh",
"shebang_with_space.sh",
"whitespace.sh",
"invalid_shebang.sh",
];
for file in &executable_files {
Command::new("git")
.args(["update-index", "--chmod=+x", file])
.current_dir(cwd.path())
.status()?;
}
// Run: should fail for partial_shebang.sh, whitespace.sh, invalid_shebang.sh
cmd_snapshot!(context.filters(), context.run(), @r#"
success: false
exit_code: 1
----- stdout -----
check that executables have shebangs.....................................Failed
- hook id: check-executables-have-shebangs
- exit code: 1
invalid_shebang.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x invalid_shebang.sh'
If on Windows, you may also need to: 'git add --chmod=-x invalid_shebang.sh'
If it is supposed to be executable, double-check its shebang.
partial_shebang.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x partial_shebang.sh'
If on Windows, you may also need to: 'git add --chmod=-x partial_shebang.sh'
If it is supposed to be executable, double-check its shebang.
whitespace.sh marked executable but has no (or invalid) shebang!
If it isn't supposed to be executable, try: 'chmod -x whitespace.sh'
If on Windows, you may also need to: 'git add --chmod=-x whitespace.sh'
If it is supposed to be executable, double-check its shebang.
----- stderr -----
"#);
Ok(())
}