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::>(); 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::>(); 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::>(); 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 `...` 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 ` 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 ----- "); }