1
0
mirror of https://github.com/j178/prek.git synced 2026-04-25 02:11:36 +02:00
Files
prek/tests/auto_update.rs
T
Copilot 011a0b3ff1 Rename crate lib/constants to crates/prek-consts and rename lib to crates (#1026)
* Initial plan

* Rename crate lib/constants to lib/prek-consts and fix all references

Co-authored-by: j178 <10510431+j178@users.noreply.github.com>

* Rename lib directory to crates and update all references

Co-authored-by: j178 <10510431+j178@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: j178 <10510431+j178@users.noreply.github.com>
2025-11-03 23:16:58 +08:00

939 lines
24 KiB
Rust

use std::process::Command;
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::ChildPath;
use assert_fs::prelude::*;
use insta::assert_snapshot;
use prek_consts::CONFIG_FILE;
use crate::common::{TestContext, cmd_snapshot};
mod common;
/// Helper function to create a local git repository with hooks
fn create_local_git_repo(context: &TestContext, repo_name: &str, tags: &[&str]) -> Result<String> {
let repo_dir = context.home_dir().child(format!("test-repos/{repo_name}"));
repo_dir.create_dir_all()?;
Command::new("git")
.arg("-c")
.arg("init.defaultBranch=master")
.arg("init")
.current_dir(&repo_dir)
.assert()
.success();
Command::new("git")
.arg("config")
.arg("user.name")
.arg("Prek Test")
.current_dir(&repo_dir)
.assert()
.success();
Command::new("git")
.arg("config")
.arg("user.email")
.arg("test@prek.dev")
.current_dir(&repo_dir)
.assert()
.success();
Command::new("git")
.arg("config")
.arg("core.autocrlf")
.arg("false")
.current_dir(&repo_dir)
.assert()
.success();
// Create .pre-commit-hooks.yaml
repo_dir
.child(".pre-commit-hooks.yaml")
.write_str(indoc::indoc! {r#"
- id: test-hook
name: Test Hook
entry: echo
language: system
- id: another-hook
name: Another Hook
entry: python3 -c 'print("hello")'
language: python
"#})?;
Command::new("git")
.arg("add")
.arg(".")
.current_dir(&repo_dir)
.assert()
.success();
Command::new("git")
.arg("commit")
.arg("-m")
.arg("Initial commit")
.current_dir(&repo_dir)
.assert()
.success();
// Create tags
for tag in tags {
Command::new("git")
.arg("commit")
.arg("-m")
.arg(format!("Release {tag}"))
.arg("--allow-empty")
.current_dir(&repo_dir)
.assert()
.success();
Command::new("git")
.arg("tag")
.arg(tag)
.arg("-m")
.arg(tag)
.current_dir(&repo_dir)
.assert()
.success();
}
// Add an extra commit to the tip
Command::new("git")
.arg("commit")
.arg("-m")
.arg("tip")
.arg("--allow-empty")
.current_dir(&repo_dir)
.assert()
.success();
Ok(repo_dir.to_string_lossy().to_string())
}
#[test]
fn auto_update_basic() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "test-repo", &["v1.0.0", "v1.1.0", "v2.0.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/test-repo] updating v1.0.0 -> v2.0.0
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: [HOME]/test-repos/test-repo
rev: v2.0.0
hooks:
- id: test-hook
"#);
}
);
Ok(())
}
#[test]
fn auto_update_already_up_to_date() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "up-to-date-repo", &["v1.0.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/up-to-date-repo] already up to date
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: [HOME]/test-repos/up-to-date-repo
rev: v1.0.0
hooks:
- id: test-hook
"#);
}
);
Ok(())
}
#[test]
fn auto_update_multiple_repos_mixed() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo1_path = create_local_git_repo(&context, "repo1", &["v1.0.0", "v1.1.0"])?;
let repo2_path = create_local_git_repo(&context, "repo2", &["v2.0.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
- repo: {}
rev: v1.0.0
hooks:
- id: same-hook
- repo: {}
rev: v2.0.0
hooks:
- id: another-hook
", repo1_path, repo1_path, repo2_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/repo1] updating v1.0.0 -> v1.1.0
[[HOME]/test-repos/repo2] already up to date
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r"
repos:
- repo: [HOME]/test-repos/repo1
rev: v1.1.0
hooks:
- id: test-hook
- repo: [HOME]/test-repos/repo1
rev: v1.1.0
hooks:
- id: same-hook
- repo: [HOME]/test-repos/repo2
rev: v2.0.0
hooks:
- id: another-hook
");
}
);
Ok(())
}
#[test]
fn auto_update_specific_repos() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo1_path = create_local_git_repo(&context, "repo1", &["v1.0.0", "v1.1.0"])?;
let repo2_path = create_local_git_repo(&context, "repo2", &["v2.0.0", "v2.1.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
- repo: {}
rev: v2.0.0
hooks:
- id: another-hook
", repo1_path, repo2_path});
context.git_add(".");
let filters = context.filters();
// Update only repo1
cmd_snapshot!(filters.clone(), context.auto_update().arg("--repo").arg(&repo1_path), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/repo1] updating v1.0.0 -> v1.1.0
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: [HOME]/test-repos/repo1
rev: v1.1.0
hooks:
- id: test-hook
- repo: [HOME]/test-repos/repo2
rev: v2.0.0
hooks:
- id: another-hook
"#);
}
);
// Update both repo1 and repo2
cmd_snapshot!(filters.clone(), context.auto_update().arg("--repo").arg(&repo1_path).arg("--repo").arg(&repo2_path), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/repo1] already up to date
[[HOME]/test-repos/repo2] updating v2.0.0 -> v2.1.0
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: [HOME]/test-repos/repo1
rev: v1.1.0
hooks:
- id: test-hook
- repo: [HOME]/test-repos/repo2
rev: v2.1.0
hooks:
- id: another-hook
"#);
}
);
Ok(())
}
#[test]
fn auto_update_bleeding_edge() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "bleeding-repo", &["v1.0.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([("[a-f0-9]{40}", "[COMMIT_SHA]")])
.collect::<Vec<_>>();
cmd_snapshot!(filters.clone(), context.auto_update().arg("--bleeding-edge"), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/bleeding-repo] updating v1.0.0 -> [COMMIT_SHA]
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: [HOME]/test-repos/bleeding-repo
rev: [COMMIT_SHA]
hooks:
- id: test-hook
"#);
}
);
Ok(())
}
#[test]
fn auto_update_freeze() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "freeze-repo", &["v1.0.0", "v1.1.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([(r" [a-f0-9]{40}", r" [COMMIT_SHA]")])
.collect::<Vec<_>>();
cmd_snapshot!(filters.clone(), context.auto_update().arg("--freeze"), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/freeze-repo] updating v1.0.0 -> [COMMIT_SHA]
----- stderr -----
"#);
// Should contain frozen comment
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r##"
repos:
- repo: [HOME]/test-repos/freeze-repo
rev: [COMMIT_SHA] # frozen: v1.1.0
hooks:
- id: test-hook
"##);
}
);
Ok(())
}
#[test]
fn auto_update_preserve_formatting() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo1_path = create_local_git_repo(&context, "repo1", &["v1.0.0", "v1.1.0"])?;
let repo2_path = create_local_git_repo(&context, "repo2", &["v1.0.0", "v1.1.0"])?;
// Use specific formatting with comments
context.write_pre_commit_config(&indoc::formatdoc! {r#"
# Pre-commit configuration
repos:
- repo: {} # Test repository
rev: 'v1.0.0' # Current version
hooks:
- id: test-hook
# Hook configuration
name: Test Hook
- repo: {}
rev: "v1.0.0" # Current version
hooks:
- id: test-hook
# Hook configuration
name: Test Hook
"#, repo1_path, repo2_path });
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/repo1] updating v1.0.0 -> v1.1.0
[[HOME]/test-repos/repo2] updating v1.0.0 -> v1.1.0
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r"
# Pre-commit configuration
repos:
- repo: [HOME]/test-repos/repo1 # Test repository
rev: v1.1.0 # Current version
hooks:
- id: test-hook
# Hook configuration
name: Test Hook
- repo: [HOME]/test-repos/repo2
rev: v1.1.0 # Current version
hooks:
- id: test-hook
# Hook configuration
name: Test Hook
");
}
);
Ok(())
}
#[test]
fn auto_update_with_existing_frozen_comment() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path =
create_local_git_repo(&context, "frozen-repo", &["v1.0.0", "v1.1.0", "v1.2.0"])?;
let commit_sha = "1234567890abcdef1234567890abcdef12345678";
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: {} # frozen: v1.0.0
hooks:
- id: test-hook
", repo_path, commit_sha});
context.git_add(".");
let filters = context
.filters()
.into_iter()
.chain([(commit_sha, "[COMMIT_SHA]")])
.collect::<Vec<_>>();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/frozen-repo] updating [COMMIT_SHA] -> v1.2.0
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: [HOME]/test-repos/frozen-repo
rev: v1.2.0
hooks:
- id: test-hook
"#);
}
);
Ok(())
}
#[test]
fn auto_update_local_repo_ignored() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "remote-repo", &["v1.0.0", "v1.1.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: local
hooks:
- id: local-hook
name: Local Hook
language: system
entry: echo
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/remote-repo] updating v1.0.0 -> v1.1.0
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: local
hooks:
- id: local-hook
name: Local Hook
language: system
entry: echo
- repo: [HOME]/test-repos/remote-repo
rev: v1.1.0
hooks:
- id: test-hook
"#);
}
);
Ok(())
}
#[test]
fn missing_hook_ids() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "missing-hook-repo", &["v1.0.0"])?;
// Remove the 'test-hook' from the hooks file
ChildPath::new(&repo_path)
.child(".pre-commit-hooks.yaml")
.write_str(indoc::indoc! {r#"
- id: another-hook
name: Another Hook
entry: python3 -c 'print("hello")'
language: python
"#})?;
Command::new("git")
.arg("add")
.arg(".")
.current_dir(&repo_path)
.assert()
.success();
Command::new("git")
.arg("commit")
.arg("-m")
.arg("Remove test-hook")
.current_dir(&repo_path)
.assert()
.success();
Command::new("git")
.arg("tag")
.arg("v2.0.0")
.arg("-m")
.arg("v2.0.0")
.current_dir(&repo_path)
.assert()
.success();
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
[[HOME]/test-repos/missing-hook-repo] update failed: Cannot update to rev `v2.0.0`, hook is missing: test-hook
"#);
Ok(())
}
#[test]
fn auto_update_workspace() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo1_path =
create_local_git_repo(&context, "workspace-repo1", &["v1.0.0", "v1.1.0", "v2.0.0"])?;
let repo2_path = create_local_git_repo(&context, "workspace-repo2", &["v1.0.0", "v1.5.0"])?;
let repo3_path = create_local_git_repo(&context, "workspace-repo3", &["v2.0.0"])?;
context.setup_workspace(
&["project-a", "project-b"],
"repos: []", // Minimal valid config for root
)?;
context
.work_dir()
.child("project-a/.pre-commit-config.yaml")
.write_str(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
- repo: {}
rev: v1.0.0
hooks:
- id: another-hook
", repo1_path, repo2_path})?;
context
.work_dir()
.child("project-b/.pre-commit-config.yaml")
.write_str(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: another-hook
- repo: {}
rev: v2.0.0
hooks:
- id: test-hook
", repo2_path, repo3_path})?;
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/workspace-repo1] updating v1.0.0 -> v2.0.0
[[HOME]/test-repos/workspace-repo2] updating v1.0.0 -> v1.5.0
[[HOME]/test-repos/workspace-repo3] already up to date
----- stderr -----
");
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read("project-a/.pre-commit-config.yaml"), @r#"
repos:
- repo: [HOME]/test-repos/workspace-repo1
rev: v2.0.0
hooks:
- id: test-hook
- repo: [HOME]/test-repos/workspace-repo2
rev: v1.5.0
hooks:
- id: another-hook
"#);
}
);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read("project-b/.pre-commit-config.yaml"), @r#"
repos:
- repo: [HOME]/test-repos/workspace-repo2
rev: v1.5.0
hooks:
- id: another-hook
- repo: [HOME]/test-repos/workspace-repo3
rev: v2.0.0
hooks:
- id: test-hook
"#);
}
);
Ok(())
}
// When there are multiple tags pointing to the same object,
// prek prefer picking a tag with a dot and is closest to the current rev according
// to Levenshtein distance.
#[test]
fn prefer_similar_tags() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "remote-repo", &["v1.0.0", "v1.1.0"])?;
// Add tag foo-v1.0.0 pointing to the same commit as v1.1.0
// v1.0.0 distance to v1.1.0 is 1
// v1.0.0 distance to foo-v1.0.0 is 4
// So we choose v1.1.0 as the update target
// But if the newest tag is v1.1.1111 (distance is 5), then we would choose foo-v1.0.0 instead
Command::new("git")
.arg("tag")
.arg("foo-v1.0.0")
.arg("-m")
.arg("foo-v1.0.0")
.arg("v1.1.0^{}")
.current_dir(&repo_path)
.assert()
.success();
// Add tag v1 pointing to the same commit as v1.1.0
Command::new("git")
.arg("tag")
.arg("v1")
.arg("-m")
.arg("v1")
.arg("v1.1.0^{}")
.current_dir(&repo_path)
.assert()
.success();
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: local
hooks:
- id: local-hook
name: Local Hook
language: system
entry: echo
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/remote-repo] updating v1.0.0 -> v1.1.0
----- stderr -----
");
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r"
repos:
- repo: local
hooks:
- id: local-hook
name: Local Hook
language: system
entry: echo
- repo: [HOME]/test-repos/remote-repo
rev: v1.1.0
hooks:
- id: test-hook
");
}
);
Ok(())
}
#[test]
fn auto_update_dry_run() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "test-repo", &["v1.0.0", "v1.1.0", "v2.0.0"])?;
context.write_pre_commit_config(&indoc::formatdoc! {r"
repos:
- repo: {}
rev: v1.0.0
hooks:
- id: test-hook
", repo_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update().arg("--dry-run"), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/test-repo] updating v1.0.0 -> v2.0.0
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r"
repos:
- repo: [HOME]/test-repos/test-repo
rev: v1.0.0
hooks:
- id: test-hook
");
}
);
Ok(())
}
#[test]
fn quoting_float_like_version_number() -> Result<()> {
let context = TestContext::new();
context.init_project();
let repo_path = create_local_git_repo(&context, "test-repo", &["0.49", "0.50"])?;
// Our serialize by default quotes this floats with single quotes, e.g., '0.49'. Use
// a different quotaing style here to validate that this does not create conflicts.
context.write_pre_commit_config(&indoc::formatdoc! {r#"
repos:
- repo: {}
rev: "0.49"
hooks:
- id: test-hook
"#, repo_path});
context.git_add(".");
let filters = context.filters();
cmd_snapshot!(filters.clone(), context.auto_update(), @r#"
success: true
exit_code: 0
----- stdout -----
[[HOME]/test-repos/test-repo] updating 0.49 -> 0.50
----- stderr -----
"#);
insta::with_settings!(
{ filters => filters.clone() },
{
assert_snapshot!(context.read(CONFIG_FILE), @r#"
repos:
- repo: [HOME]/test-repos/test-repo
rev: '0.50'
hooks:
- id: test-hook
"#);
}
);
Ok(())
}