mod common;
use common::{
add_local_remote, git_cmd, git_log, git_push_to_remote, run_cursus, temp_git_repo,
temp_git_repo_with_cargo_workspace, temp_git_repo_with_project, temp_real_git_repo_with_config,
write_changeset,
};
use cursus::model::config::{
CargoConfig, Config, GitConfig, GitHubConfig, GlobalConfig, PackageManager,
};
use tempfile::TempDir;
async fn temp_git_repo_with_project_in_unicode_dir(prefix: &str) -> TempDir {
let dir = tempfile::Builder::new()
.prefix(prefix)
.tempdir()
.expect("Failed to create unicode temp dir");
std::fs::create_dir(dir.path().join(".git")).unwrap();
let env = common::test_env(dir.path());
let config = Config::new().with_cargo(CargoConfig::enabled());
config.save(env.fs(), env.git().path()).await.unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"test-project\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(dir.path().join("src/lib.rs"), "").unwrap();
dir
}
fn read_changeset_files(dir: &std::path::Path) -> Vec<String> {
let cursus_dir = dir.join(".cursus");
if !cursus_dir.exists() {
return vec![];
}
std::fs::read_dir(&cursus_dir)
.expect("Failed to read .cursus dir")
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension()?.to_str()? == "md" {
Some(std::fs::read_to_string(&path).expect("Failed to read changeset file"))
} else {
None
}
})
.collect()
}
fn write_config(dir: &std::path::Path, toml: &str) {
let config_dir = dir.join(".cursus");
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(config_dir.join("config.toml"), toml).unwrap();
}
fn git_commit_all(dir: &std::path::Path, message: &str) {
git_cmd(dir, &["add", "."]);
git_cmd(dir, &["commit", "-m", message]);
}
fn setup_single_cargo_package(dir: &std::path::Path, name: &str, version: &str) {
std::fs::write(
dir.join("Cargo.toml"),
format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\nedition = \"2024\"\n"),
)
.unwrap();
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(dir.join("src/lib.rs"), "").unwrap();
git_commit_all(dir, "chore: add package");
}
async fn assert_change_preserves_message(change_type: &str, message: &str) {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
let result = run_cursus(
[
"cursus",
"--no-interactive",
"change",
"-t",
change_type,
"-m",
message,
],
dir.path(),
)
.await;
assert!(result.is_ok(), "Expected success: {result:?}");
let files = read_changeset_files(dir.path());
assert!(!files.is_empty(), "Expected at least one changeset file");
assert!(
files.iter().any(|content| content.contains(message)),
"Expected message {:?} in changeset, got: {files:?}",
message
);
}
#[tokio::test]
async fn change_with_emoji_message() {
assert_change_preserves_message("minor", "🎉 Added internationalization support").await;
}
#[tokio::test]
async fn change_with_cjk_message() {
assert_change_preserves_message("minor", "新機能を追加しました").await;
}
#[tokio::test]
async fn change_with_mixed_script_message() {
assert_change_preserves_message("patch", "Ändere die Konfiguration für café").await;
}
#[tokio::test]
async fn change_with_rtl_message() {
assert_change_preserves_message("minor", "إضافة ميزة جديدة").await;
}
#[tokio::test]
async fn change_with_combining_characters_message() {
assert_change_preserves_message("patch", "Update cafe\u{0301} configuration").await;
}
#[tokio::test]
async fn prepare_preserves_emoji_in_changelog() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
let message = "🎉 Added internationalization support";
write_changeset(
dir.path(),
"change.md",
&format!("+++\ntest-project = \"minor\"\n+++\n\n{message}\n"),
);
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "Expected success: {result:?}");
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md"))
.expect("CHANGELOG.md should exist after prepare");
assert!(
changelog.contains(message),
"Expected emoji message in changelog, got:\n{changelog}"
);
}
#[tokio::test]
async fn prepare_preserves_cjk_in_changelog() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
let message = "新機能を追加しました";
write_changeset(
dir.path(),
"change.md",
&format!("+++\ntest-project = \"minor\"\n+++\n\n{message}\n"),
);
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "Expected success: {result:?}");
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md"))
.expect("CHANGELOG.md should exist after prepare");
assert!(
changelog.contains(message),
"Expected CJK message in changelog, got:\n{changelog}"
);
}
#[tokio::test]
async fn prepare_preserves_mixed_unicode_across_sections() {
let dir = temp_git_repo_with_cargo_workspace(&[("alpha", "0.1.0"), ("beta", "0.1.0")]).await;
let minor_message = "Füge neue Konfigurationsoptionen hinzu";
let patch_message = "Исправить ошибку в обработке запросов";
write_changeset(
dir.path(),
"feature.md",
&format!("+++\nalpha = \"minor\"\n+++\n\n{minor_message}\n"),
);
write_changeset(
dir.path(),
"fix.md",
&format!("+++\nbeta = \"patch\"\n+++\n\n{patch_message}\n"),
);
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "Expected success: {result:?}");
let alpha_changelog = std::fs::read_to_string(dir.path().join("alpha/CHANGELOG.md"))
.expect("alpha/CHANGELOG.md should exist after prepare");
assert!(
alpha_changelog.contains(minor_message),
"Expected Latin-accent message in alpha changelog, got:\n{alpha_changelog}"
);
let beta_changelog = std::fs::read_to_string(dir.path().join("beta/CHANGELOG.md"))
.expect("beta/CHANGELOG.md should exist after prepare");
assert!(
beta_changelog.contains(patch_message),
"Expected Cyrillic message in beta changelog, got:\n{beta_changelog}"
);
}
#[tokio::test]
async fn prepare_preserves_message_with_pr_like_pattern_and_unicode() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
let message = "Fix for café (#42)";
write_changeset(
dir.path(),
"change.md",
&format!("+++\ntest-project = \"patch\"\n+++\n\n{message}\n"),
);
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "Expected success: {result:?}");
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md"))
.expect("CHANGELOG.md should exist");
assert!(
changelog.contains("café"),
"Expected 'café' preserved in changelog near PR pattern, got:\n{changelog}"
);
}
#[tokio::test]
async fn prepare_preserves_multiline_unicode_message() {
let dir = temp_git_repo_with_project(PackageManager::Cargo).await;
let line1 = "First line: 新機能";
let line2 = "Second line: Ändere";
let line3 = "Third line: إضافة";
let message = format!("{line1}\n{line2}\n{line3}");
write_changeset(
dir.path(),
"change.md",
&format!("+++\ntest-project = \"minor\"\n+++\n\n{message}\n"),
);
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "Expected success: {result:?}");
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md"))
.expect("CHANGELOG.md should exist");
assert!(
changelog.contains(line1),
"Expected CJK first line in changelog, got:\n{changelog}"
);
assert!(
changelog.contains(line2),
"Expected Latin second line in changelog, got:\n{changelog}"
);
assert!(
changelog.contains(line3),
"Expected Arabic third line in changelog, got:\n{changelog}"
);
}
#[tokio::test]
async fn change_in_unicode_directory_cjk() {
let dir = temp_git_repo_with_project_in_unicode_dir("cursus-\u{30C6}\u{30B9}\u{30C8}-").await;
let result = run_cursus(
[
"cursus",
"--no-interactive",
"change",
"-t",
"minor",
"-m",
"test change",
],
dir.path(),
)
.await;
assert!(
result.is_ok(),
"Expected success in CJK directory: {result:?}"
);
let files = read_changeset_files(dir.path());
assert!(!files.is_empty(), "Expected a changeset file");
}
#[tokio::test]
async fn change_in_unicode_directory_emoji() {
let dir = temp_git_repo_with_project_in_unicode_dir("cursus-\u{1F680}-").await;
let result = run_cursus(
[
"cursus",
"--no-interactive",
"change",
"-t",
"patch",
"-m",
"test change",
],
dir.path(),
)
.await;
assert!(
result.is_ok(),
"Expected success in emoji directory: {result:?}"
);
let files = read_changeset_files(dir.path());
assert!(!files.is_empty(), "Expected a changeset file");
}
#[tokio::test]
async fn prepare_in_unicode_directory() {
let dir = temp_git_repo_with_project_in_unicode_dir("cursus-caf\u{00E9}-").await;
let message = "A feature for café users";
write_changeset(
dir.path(),
"change.md",
&format!("+++\ntest-project = \"minor\"\n+++\n\n{message}\n"),
);
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(
result.is_ok(),
"Expected prepare to succeed in unicode directory: {result:?}"
);
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md"))
.expect("CHANGELOG.md should exist");
assert!(
changelog.contains(message),
"Expected message in changelog, got:\n{changelog}"
);
let cargo_toml =
std::fs::read_to_string(dir.path().join("Cargo.toml")).expect("Cargo.toml should exist");
assert!(
cargo_toml.contains("0.2.0"),
"Expected version bump to 0.2.0 in Cargo.toml, got:\n{cargo_toml}"
);
}
#[tokio::test]
async fn config_with_unicode_ignore_pattern() {
let dir =
temp_git_repo_with_cargo_workspace(&[("app", "0.1.0"), ("biblioth\u{00E8}que", "0.1.0")])
.await;
let global = GlobalConfig {
ignore: vec!["biblioth\u{00E8}que".to_string()],
..Default::default()
};
let env2 = common::test_env(dir.path());
let config = Config::new()
.with_global(global)
.with_cargo(CargoConfig::enabled());
config.save(env2.fs(), env2.git().path()).await.unwrap();
let result = run_cursus(
[
"cursus",
"--no-interactive",
"change",
"-t",
"minor",
"-m",
"test",
"-p",
"app",
],
dir.path(),
)
.await;
assert!(
result.is_ok(),
"Expected success targeting non-ignored package: {result:?}"
);
let result = run_cursus(
[
"cursus",
"--no-interactive",
"change",
"-t",
"minor",
"-m",
"test",
"-p",
"biblioth\u{00E8}que",
],
dir.path(),
)
.await;
assert!(
result.is_err(),
"Expected error when targeting unicode-named ignored package"
);
}
#[tokio::test]
async fn config_with_unicode_subfolder_path() {
let dir = temp_git_repo();
let subfolder = "donn\u{00E9}es";
let env = common::test_env(dir.path());
let mut config = Config::new().with_cargo(CargoConfig::enabled());
config.cargo.path = Some(subfolder.to_string());
config.save(env.fs(), env.git().path()).await.unwrap();
let sub_path = dir.path().join(subfolder);
std::fs::create_dir_all(&sub_path).unwrap();
std::fs::write(
sub_path.join("Cargo.toml"),
"[package]\nname = \"test-project\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
let result = run_cursus(
[
"cursus",
"--no-interactive",
"change",
"-t",
"minor",
"-m",
"test",
],
dir.path(),
)
.await;
assert!(
result.is_ok(),
"Expected success with unicode subfolder path: {result:?}"
);
}
#[tokio::test]
async fn change_targets_unicode_project_name() {
let dir =
temp_git_repo_with_cargo_workspace(&[("donn\u{00E9}es", "0.1.0"), ("app", "0.1.0")]).await;
let result = run_cursus(
[
"cursus",
"--no-interactive",
"change",
"-t",
"minor",
"-m",
"test",
"-p",
"donn\u{00E9}es",
],
dir.path(),
)
.await;
assert!(
result.is_ok(),
"Expected success targeting unicode package name: {result:?}"
);
let files = read_changeset_files(dir.path());
assert!(
files
.iter()
.any(|content| content.contains("donn\u{00E9}es")),
"Expected unicode package name in changeset frontmatter, got: {files:?}"
);
}
#[tokio::test]
async fn prepare_git_unicode_commit_message() {
let commit_msg = "ci: mise \u{00E0} jour des versions \u{1F680}";
let config = GitConfig::enabled_config().with_prepare_commit_message(commit_msg.to_string());
let dir = temp_real_git_repo_with_config(PackageManager::Cargo, config).await;
setup_single_cargo_package(dir.path(), "my-pkg", "1.0.0");
write_changeset(
dir.path(),
"change.md",
"+++\nmy-pkg = \"patch\"\n+++\n\nA fix\n",
);
git_commit_all(dir.path(), "chore: add changeset");
let _remote = add_local_remote(dir.path());
git_push_to_remote(dir.path());
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(result.is_ok(), "Expected success: {result:?}");
let log = git_log(dir.path());
assert!(
log[0].contains(commit_msg),
"Expected unicode commit message, got: {}",
log[0]
);
}
#[tokio::test]
async fn github_config_unicode_pr_title_loads() {
let dir = temp_git_repo();
let env = common::test_env(dir.path());
let config = Config::new()
.with_cargo(CargoConfig::enabled())
.with_github(
GitHubConfig::enabled_config()
.with_pull_request_title("Mise \u{00E0} jour des versions \u{1F389}".to_string()),
);
config.save(env.fs(), env.git().path()).await.unwrap();
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let result = run_cursus(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
)
.await;
assert!(
result.is_ok(),
"Expected config to load with unicode PR title: {result:?}"
);
}
#[tokio::test]
async fn github_config_unicode_artifact_name_loads() {
let dir = temp_git_repo();
write_config(
dir.path(),
"[cargo]\nenabled = true\n[github]\nenabled = true\nowner = \"acme\"\nrepo = \"app\"\n[github.artifacts.my-app]\n\"bin\u{00E1}rio-linux\" = \"target/release/app\"\n",
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"my-app\"\nversion = \"1.0.0\"\n",
)
.unwrap();
std::fs::write(dir.path().join("CHANGELOG.md"), "# Changelog\n").unwrap();
let result = run_cursus(
["cursus", "publish", "--dry-run", "--no-interactive"],
dir.path(),
)
.await;
assert!(
result.is_ok(),
"Expected config to load with unicode artifact name: {result:?}"
);
}
#[tokio::test]
async fn change_and_prepare_unicode_path_and_content() {
let dir = temp_git_repo_with_project_in_unicode_dir("cursus-\u{1F30D}-").await;
let message = "🌍 Support for international users";
write_changeset(
dir.path(),
"change.md",
&format!("+++\ntest-project = \"minor\"\n+++\n\n{message}\n"),
);
let result = run_cursus(["cursus", "--no-interactive", "prepare"], dir.path()).await;
assert!(
result.is_ok(),
"Expected success with unicode path and content: {result:?}"
);
let changelog = std::fs::read_to_string(dir.path().join("CHANGELOG.md"))
.expect("CHANGELOG.md should exist");
assert!(
changelog.contains(message),
"Expected emoji message in changelog from unicode dir, got:\n{changelog}"
);
}