use std::collections::HashMap;
use rstest::rstest;
use worktrunk::config::expand_template;
use worktrunk::git::Repository;
use crate::common::{TestRepo, repo};
fn hash_port(s: &str) -> u16 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
s.hash(&mut h);
10000 + (h.finish() % 10000) as u16
}
#[rstest]
fn test_doc_basic_variables(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("repo", "myproject");
vars.insert("branch", "feature/auth");
vars.insert("worktree", "/home/user/myproject.feature-auth");
vars.insert("default_branch", "main");
assert_eq!(
expand_template("{{ repo }}", &vars, false, &repository, "test").unwrap(),
"myproject"
);
assert_eq!(
expand_template("{{ branch }}", &vars, false, &repository, "test").unwrap(),
"feature/auth"
);
assert_eq!(
expand_template("{{ worktree }}", &vars, false, &repository, "test").unwrap(),
"/home/user/myproject.feature-auth"
);
assert_eq!(
expand_template("{{ default_branch }}", &vars, false, &repository, "test").unwrap(),
"main"
);
}
#[rstest]
fn test_doc_sanitize_filter(repo: TestRepo) {
let mut vars = HashMap::new();
let repository = Repository::at(repo.root_path()).unwrap();
vars.insert("branch", "feature/foo");
assert_eq!(
expand_template("{{ branch | sanitize }}", &vars, false, &repository, "test").unwrap(),
"feature-foo",
"sanitize should replace / with -"
);
vars.insert("branch", "user\\task");
assert_eq!(
expand_template("{{ branch | sanitize }}", &vars, false, &repository, "test").unwrap(),
"user-task",
"sanitize should replace \\ with -"
);
vars.insert("branch", "user/feature/task");
assert_eq!(
expand_template("{{ branch | sanitize }}", &vars, false, &repository, "test").unwrap(),
"user-feature-task",
"sanitize should handle multiple slashes"
);
}
#[rstest]
fn test_doc_sanitize_db_filter(repo: TestRepo) {
let mut vars = HashMap::new();
let repository = Repository::at(repo.root_path()).unwrap();
vars.insert("branch", "feature/auth-oauth2");
let result = expand_template(
"{{ branch | sanitize_db }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert!(
result.starts_with("feature_auth_oauth2_"),
"sanitize_db should replace non-alphanumeric with _ and lowercase, got: {result}"
);
vars.insert("branch", "123-bug-fix");
let result = expand_template(
"{{ branch | sanitize_db }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert!(
result.starts_with("_123_bug_fix_"),
"sanitize_db should prefix leading digits with _, got: {result}"
);
vars.insert("branch", "UPPERCASE.Branch");
let result = expand_template(
"{{ branch | sanitize_db }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert!(
result.starts_with("uppercase_branch_"),
"sanitize_db should convert to lowercase, got: {result}"
);
vars.insert("branch", "a--b//c");
let result = expand_template(
"{{ branch | sanitize_db }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert!(
result.starts_with("a_b_c_"),
"sanitize_db should collapse consecutive underscores, got: {result}"
);
vars.insert("branch", "a-b");
let result1 = expand_template(
"{{ branch | sanitize_db }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
vars.insert("branch", "a_b");
let result2 = expand_template(
"{{ branch | sanitize_db }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert_ne!(
result1, result2,
"a-b and a_b should produce different outputs"
);
}
#[rstest]
fn test_doc_sanitize_db_truncation(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
let long_branch = "a".repeat(100);
vars.insert("branch", long_branch.as_str());
let result = expand_template(
"{{ branch | sanitize_db }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert_eq!(
result.len(),
63,
"sanitize_db should truncate to 63 characters"
);
}
#[rstest]
fn test_doc_hash_port_filter(repo: TestRepo) {
let mut vars = HashMap::new();
vars.insert("branch", "feature-foo");
let repository = Repository::at(repo.root_path()).unwrap();
let result = expand_template(
"{{ branch | hash_port }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
let port: u16 = result.parse().expect("hash_port should produce a number");
assert!(
(10000..20000).contains(&port),
"hash_port should produce port in range 10000-19999, got {port}"
);
let result2 = expand_template(
"{{ branch | hash_port }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert_eq!(result, result2, "hash_port should be deterministic");
}
#[rstest]
fn test_doc_hash_port_concatenation_precedence(repo: TestRepo) {
let mut vars = HashMap::new();
vars.insert("branch", "feature");
let repository = Repository::at(repo.root_path()).unwrap();
let with_parens = expand_template(
"{{ ('db-' ~ branch) | hash_port }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
let port_with_parens: u16 = with_parens.parse().unwrap();
let expected_port = hash_port("db-feature");
assert_eq!(
port_with_parens, expected_port,
"('db-' ~ branch) | hash_port should hash 'db-feature', not just 'feature'"
);
let without_parens = expand_template(
"{{ 'db-' ~ branch | hash_port }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
let port_just_branch = hash_port("feature");
assert_eq!(
without_parens,
format!("db-{}", port_just_branch),
"Without parens, 'db-' ~ branch | hash_port means 'db-' ~ (hash_port(branch))"
);
assert_ne!(
with_parens, without_parens,
"Parentheses change the result - this is the PR #373 issue"
);
}
#[rstest]
fn test_doc_hash_port_repo_branch_concatenation(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("repo", "myapp");
vars.insert("branch", "feature");
let result = expand_template(
"{{ (repo ~ '-' ~ branch) | hash_port }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
let port: u16 = result.parse().unwrap();
let expected = hash_port("myapp-feature");
assert_eq!(
port, expected,
"Should hash the concatenated string 'myapp-feature'"
);
}
#[rstest]
fn test_doc_example_docker_postgres(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("repo", "myproject");
vars.insert("branch", "feature-auth");
let template = r#"docker run -d --rm \
--name {{ repo }}-{{ branch | sanitize }}-postgres \
-p {{ ('db-' ~ branch) | hash_port }}:5432 \
postgres:16"#;
let result = expand_template(template, &vars, false, &repository, "test").unwrap();
assert!(
result.contains("--name myproject-feature-auth-postgres"),
"Container name should use sanitized branch"
);
let expected_port = hash_port("db-feature-auth");
assert!(
result.contains(&format!("-p {expected_port}:5432")),
"Port should be hash of 'db-feature-auth', expected {expected_port}"
);
}
#[rstest]
fn test_doc_example_database_url(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("repo", "myproject");
vars.insert("branch", "feature");
let template = "DATABASE_URL=postgres://postgres:dev@localhost:{{ ('db-' ~ branch) | hash_port }}/{{ repo }}";
let result = expand_template(template, &vars, false, &repository, "test").unwrap();
let expected_port = hash_port("db-feature");
assert_eq!(
result,
format!("DATABASE_URL=postgres://postgres:dev@localhost:{expected_port}/myproject")
);
}
#[rstest]
fn test_doc_example_dev_server(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("branch", "feature-auth");
let template = "npm run dev -- --host {{ branch }}.localhost --port {{ branch | hash_port }}";
let result = expand_template(template, &vars, false, &repository, "test").unwrap();
let expected_port = hash_port("feature-auth");
assert_eq!(
result,
format!("npm run dev -- --host feature-auth.localhost --port {expected_port}")
);
}
#[rstest]
fn test_doc_example_worktree_path_sanitize(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("branch", "feature/user/auth");
vars.insert("main_worktree", "/home/user/project");
let template = "{{ main_worktree }}.{{ branch | sanitize }}";
let result = expand_template(template, &vars, false, &repository, "test").unwrap();
assert_eq!(result, "/home/user/project.feature-user-auth");
}
#[rstest]
fn test_doc_hash_port_empty_string(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("branch", "");
let result = expand_template(
"{{ branch | hash_port }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
let port: u16 = result.parse().unwrap();
assert!(
(10000..20000).contains(&port),
"hash_port of empty string should still produce valid port"
);
}
#[rstest]
fn test_doc_sanitize_no_slashes(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("branch", "simple-branch");
let result =
expand_template("{{ branch | sanitize }}", &vars, false, &repository, "test").unwrap();
assert_eq!(
result, "simple-branch",
"sanitize should be no-op without slashes"
);
}
#[rstest]
fn test_doc_combined_filters(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let mut vars = HashMap::new();
vars.insert("branch", "feature/auth");
let result = expand_template(
"{{ branch | sanitize | hash_port }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
let port: u16 = result.parse().unwrap();
let expected = hash_port("feature-auth");
assert_eq!(port, expected);
}
#[rstest]
fn test_worktree_path_of_branch_function_registered(repo: TestRepo) {
let repository = Repository::at(repo.root_path()).unwrap();
let vars: HashMap<&str, &str> = HashMap::new();
let result = expand_template(
"{{ worktree_path_of_branch('nonexistent') }}",
&vars,
false,
&repository,
"test",
);
assert_eq!(result.unwrap(), "");
}
#[rstest]
fn test_worktree_path_of_branch_shell_escape(repo: TestRepo) {
let worktree_path = repo.root_path().parent().unwrap().join("My Worktree");
repo.run_git(&["branch", "test-branch"]);
repo.run_git(&[
"worktree",
"add",
worktree_path.to_str().unwrap(),
"test-branch",
]);
let repository = Repository::at(repo.root_path()).unwrap();
let vars: HashMap<&str, &str> = HashMap::new();
let result_literal = expand_template(
"{{ worktree_path_of_branch('test-branch') }}",
&vars,
false,
&repository,
"test",
)
.unwrap();
assert!(
result_literal.contains("My Worktree"),
"Expected literal path with space, got: {result_literal}"
);
let result_escaped = expand_template(
"{{ worktree_path_of_branch('test-branch') }}",
&vars,
true,
&repository,
"test",
)
.unwrap();
assert!(
result_escaped.contains('\'') || result_escaped.contains('\\'),
"Expected shell-escaped path, got: {result_escaped}"
);
assert!(
result_escaped.contains("My Worktree") || result_escaped.contains("My\\ Worktree"),
"Escaped path should reference worktree: {result_escaped}"
);
repo.run_git(&[
"worktree",
"remove",
"--force",
worktree_path.to_str().unwrap(),
]);
}