#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod analyzer_tests {
use crate::changes::ChangesAnalyzer;
use crate::config::PackageToolsConfig;
use std::fs;
use std::path::PathBuf;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::FileSystemManager;
use tempfile::TempDir;
async fn create_test_repo_with_commit() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
let repo = Repo::create(repo_path.to_str().unwrap()).unwrap();
let package_json = r#"{
"name": "test-package",
"version": "1.0.0"
}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
repo.add_all().unwrap();
repo.commit("Initial commit").unwrap();
(temp_dir, repo_path)
}
#[tokio::test]
async fn test_analyzer_initialization_integration() {
let (_temp, repo_path) = create_test_repo_with_commit().await;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await;
assert!(analyzer.is_ok());
}
#[tokio::test]
async fn test_analyzer_with_monorepo_integration() {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().to_path_buf();
let root_package = r#"{
"name": "monorepo-root",
"version": "1.0.0",
"workspaces": ["packages/*"]
}"#;
fs::write(workspace_path.join("package.json"), root_package).unwrap();
let pnpm_workspace = "packages:\n - 'packages/*'\n";
fs::write(workspace_path.join("pnpm-workspace.yaml"), pnpm_workspace).unwrap();
fs::create_dir_all(workspace_path.join("packages/pkg-a")).unwrap();
let pkg_a = r#"{"name": "@test/pkg-a", "version": "1.0.0"}"#;
fs::write(workspace_path.join("packages/pkg-a/package.json"), pkg_a).unwrap();
let repo = Repo::create(workspace_path.to_str().unwrap()).unwrap();
repo.add_all().unwrap();
repo.commit("Initial monorepo").unwrap();
let git_repo = Repo::open(workspace_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_path.clone(), git_repo, fs, config).await.unwrap();
assert!(analyzer.is_monorepo());
assert_eq!(analyzer.workspace_root(), workspace_path.as_path());
}
#[tokio::test]
async fn test_analyzer_configuration_integration() {
let (_temp, repo_path) = create_test_repo_with_commit().await;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let mut config = PackageToolsConfig::default();
config.changeset.path = ".custom-changesets".to_string();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
assert_eq!(analyzer.config().changeset.path, ".custom-changesets");
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod commit_range_tests {
use crate::changes::{AnalysisMode, ChangesAnalyzer};
use crate::config::PackageToolsConfig;
use std::fs;
use std::path::PathBuf;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::FileSystemManager;
use tempfile::TempDir;
async fn create_test_repo_with_commits() -> (TempDir, PathBuf, Repo) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
let repo = Repo::create(repo_path.to_str().unwrap()).unwrap();
repo.config("Test User", "test@example.com").unwrap();
let package_json = r#"{
"name": "test-package",
"version": "1.0.0"
}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
repo.add_all().unwrap();
repo.commit("Initial commit").unwrap();
fs::create_dir_all(repo_path.join("src")).unwrap();
fs::write(repo_path.join("src/index.js"), "console.log('hello');").unwrap();
repo.add_all().unwrap();
repo.commit("feat: add index.js").unwrap();
fs::write(repo_path.join("src/index.js"), "console.log('hello world');").unwrap();
repo.add_all().unwrap();
repo.commit("fix: update message").unwrap();
(temp_dir, repo_path, repo)
}
async fn create_monorepo_with_commits() -> (TempDir, PathBuf, Repo) {
let temp_dir = TempDir::new().unwrap();
let workspace_path = temp_dir.path().to_path_buf();
let root_package = r#"{
"name": "monorepo-root",
"version": "1.0.0",
"workspaces": ["packages/*"]
}"#;
fs::write(workspace_path.join("package.json"), root_package).unwrap();
let pnpm_workspace = "packages:\n - 'packages/*'\n";
fs::write(workspace_path.join("pnpm-workspace.yaml"), pnpm_workspace).unwrap();
fs::create_dir_all(workspace_path.join("packages/pkg-a")).unwrap();
let pkg_a = r#"{"name": "@test/pkg-a", "version": "1.0.0"}"#;
fs::write(workspace_path.join("packages/pkg-a/package.json"), pkg_a).unwrap();
fs::create_dir_all(workspace_path.join("packages/pkg-b")).unwrap();
let pkg_b = r#"{"name": "@test/pkg-b", "version": "1.0.0"}"#;
fs::write(workspace_path.join("packages/pkg-b/package.json"), pkg_b).unwrap();
let repo = Repo::create(workspace_path.to_str().unwrap()).unwrap();
repo.config("Test User", "test@example.com").unwrap();
repo.add_all().unwrap();
repo.commit("Initial monorepo").unwrap();
fs::create_dir_all(workspace_path.join("packages/pkg-a/src")).unwrap();
fs::write(workspace_path.join("packages/pkg-a/src/index.js"), "export const a = 1;")
.unwrap();
repo.add_all().unwrap();
repo.commit("feat: add pkg-a index").unwrap();
fs::create_dir_all(workspace_path.join("packages/pkg-b/src")).unwrap();
fs::write(workspace_path.join("packages/pkg-b/src/index.js"), "export const b = 2;")
.unwrap();
repo.add_all().unwrap();
repo.commit("feat: add pkg-b index").unwrap();
fs::write(workspace_path.join("packages/pkg-a/src/index.js"), "export const a = 10;")
.unwrap();
fs::write(workspace_path.join("packages/pkg-b/src/index.js"), "export const b = 20;")
.unwrap();
repo.add_all().unwrap();
repo.commit("fix: update both packages").unwrap();
(temp_dir, workspace_path, repo)
}
#[allow(clippy::len_zero)]
#[tokio::test]
async fn test_analyze_commit_range_single_package() {
let (_temp, repo_path, repo) = create_test_repo_with_commits().await;
let commits = repo.get_commits_since(None, &None).unwrap();
assert!(commits.len() >= 2);
let first_commit = &commits[commits.len() - 1].hash;
let last_commit = "HEAD";
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path.clone(), git_repo, fs, config).await.unwrap();
let report = analyzer.analyze_commit_range(first_commit, last_commit).await.unwrap();
assert_eq!(report.analysis_mode, AnalysisMode::CommitRange);
assert_eq!(report.base_ref, Some(first_commit.clone()));
assert_eq!(report.head_ref, Some(last_commit.to_string()));
assert!(report.has_changes());
assert!(report.packages_with_changes().len() > 0);
}
#[tokio::test]
async fn test_analyze_commit_range_with_commits() {
let (_temp, repo_path, repo) = create_test_repo_with_commits().await;
let commits = repo.get_commits_since(None, &None).unwrap();
let first_commit = &commits[commits.len() - 1].hash;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let report = analyzer.analyze_commit_range(first_commit, "HEAD").await.unwrap();
let packages_with_changes = report.packages_with_changes();
assert!(!packages_with_changes.is_empty());
for package in packages_with_changes {
assert!(!package.commits.is_empty(), "Package should have commits");
for commit in &package.commits {
assert!(!commit.hash.is_empty());
assert!(!commit.short_hash.is_empty());
assert!(!commit.message.is_empty());
}
}
}
#[tokio::test]
async fn test_analyze_commit_range_monorepo() {
let (_temp, workspace_path, repo) = create_monorepo_with_commits().await;
let commits = repo.get_commits_since(None, &None).unwrap();
let first_commit = &commits[commits.len() - 1].hash;
let git_repo = Repo::open(workspace_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(workspace_path, git_repo, fs, config).await.unwrap();
let report = analyzer.analyze_commit_range(first_commit, "HEAD").await.unwrap();
assert!(report.is_monorepo);
assert!(report.has_changes());
let packages_with_changes = report.packages_with_changes();
assert!(packages_with_changes.len() >= 2, "Should have at least 2 packages with changes");
for package in packages_with_changes {
if package.package_name().contains("pkg-a") || package.package_name().contains("pkg-b")
{
assert!(
!package.commits.is_empty(),
"Package {} should have commits",
package.package_name()
);
}
}
}
#[tokio::test]
async fn test_analyze_commit_range_multi_package_commit() {
let (_temp, workspace_path, repo) = create_monorepo_with_commits().await;
let commits = repo.get_commits_since(None, &None).unwrap();
let last_commit = &commits[0];
assert!(last_commit.message.contains("update both packages"));
let penultimate_commit = &commits[1].hash;
let git_repo = Repo::open(workspace_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(workspace_path, git_repo, fs, config).await.unwrap();
let report = analyzer.analyze_commit_range(penultimate_commit, "HEAD").await.unwrap();
let mut pkg_a_found = false;
let mut pkg_b_found = false;
for package in report.packages_with_changes() {
if package.package_name().contains("pkg-a") {
pkg_a_found = true;
let affecting_commit =
package.commits.iter().find(|c| c.message.contains("update both packages"));
assert!(affecting_commit.is_some(), "pkg-a should have the multi-package commit");
if let Some(commit) = affecting_commit {
assert!(
commit.affected_packages.len() >= 2,
"Commit should list multiple affected packages"
);
}
}
if package.package_name().contains("pkg-b") {
pkg_b_found = true;
}
}
assert!(pkg_a_found && pkg_b_found, "Both packages should have changes");
}
#[tokio::test]
async fn test_analyze_commit_range_file_to_commit_association() {
let (_temp, repo_path, repo) = create_test_repo_with_commits().await;
let commits = repo.get_commits_since(None, &None).unwrap();
let first_commit = &commits[commits.len() - 1].hash;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let report = analyzer.analyze_commit_range(first_commit, "HEAD").await.unwrap();
let packages_with_changes = report.packages_with_changes();
assert!(!packages_with_changes.is_empty());
for package in packages_with_changes {
for file in &package.files {
assert!(
!file.commits.is_empty(),
"File {} should have associated commits",
file.path.display()
);
}
}
}
#[tokio::test]
async fn test_analyze_commit_range_empty_range() {
let (_temp, repo_path, repo) = create_test_repo_with_commits().await;
let commits = repo.get_commits_since(None, &None).unwrap();
let commit = &commits[0].hash;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let result = analyzer.analyze_commit_range(commit, commit).await;
assert!(result.is_err(), "Empty commit range should return an error");
}
#[tokio::test]
async fn test_analyze_commit_range_invalid_ref() {
let (_temp, repo_path, _repo) = create_test_repo_with_commits().await;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let result = analyzer.analyze_commit_range("invalid-ref", "HEAD").await;
assert!(result.is_err(), "Invalid ref should return an error");
}
#[tokio::test]
async fn test_analyze_commit_range_branch_comparison() {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
let repo = Repo::create(repo_path.to_str().unwrap()).unwrap();
repo.config("Test User", "test@example.com").unwrap();
let package_json = r#"{"name": "test-pkg", "version": "1.0.0"}"#;
fs::write(repo_path.join("package.json"), package_json).unwrap();
repo.add_all().unwrap();
repo.commit("Initial commit").unwrap();
repo.create_branch("feature").unwrap();
repo.checkout("feature").unwrap();
fs::write(repo_path.join("feature.js"), "// feature").unwrap();
repo.add_all().unwrap();
repo.commit("feat: add feature").unwrap();
repo.checkout("main").unwrap();
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let report = analyzer.analyze_commit_range("main", "feature").await.unwrap();
assert_eq!(report.base_ref, Some("main".to_string()));
assert_eq!(report.head_ref, Some("feature".to_string()));
assert!(report.has_changes());
let packages = report.packages_with_changes();
assert!(!packages.is_empty());
assert!(!packages[0].files.is_empty());
}
#[tokio::test]
async fn test_commit_info_metadata() {
let (_temp, repo_path, _repo) = create_test_repo_with_commits().await;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let commits = analyzer.git_repo().get_commits_since(None, &None).unwrap();
let first_commit = &commits[commits.len() - 1].hash;
let report = analyzer.analyze_commit_range(first_commit, "HEAD").await.unwrap();
let packages = report.packages_with_changes();
assert!(!packages.is_empty());
for package in packages {
for commit in &package.commits {
assert!(!commit.hash.is_empty(), "Commit hash should not be empty");
assert_eq!(commit.short_hash.len(), 7, "Short hash should be 7 characters");
assert!(!commit.author.is_empty(), "Author should not be empty");
assert!(commit.author_email.contains('@'), "Email should contain @");
assert!(!commit.message.is_empty(), "Message should not be empty");
assert!(!commit.full_message.is_empty(), "Full message should not be empty");
}
}
}
#[tokio::test]
async fn test_commit_range_statistics() {
let (_temp, repo_path, repo) = create_test_repo_with_commits().await;
let commits = repo.get_commits_since(None, &None).unwrap();
let first_commit = &commits[commits.len() - 1].hash;
let git_repo = Repo::open(repo_path.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::new(repo_path, git_repo, fs, config).await.unwrap();
let report = analyzer.analyze_commit_range(first_commit, "HEAD").await.unwrap();
assert!(report.summary.total_packages > 0);
assert!(report.summary.packages_with_changes > 0);
assert!(report.summary.total_files_changed > 0);
assert!(report.summary.total_commits > 0);
for package in report.packages_with_changes() {
assert!(package.stats.commits > 0, "Package should have commit count");
assert!(package.stats.files_changed > 0, "Package should have file count");
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
#[allow(clippy::panic)]
mod mapping_tests {
use crate::changes::mapping::PackageMapper;
use crate::error::ChangesError;
use std::path::PathBuf;
use sublime_standard_tools::filesystem::{AsyncFileSystem, FileSystemManager};
use tempfile::TempDir;
use tokio;
async fn create_single_package_workspace() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let workspace_root = temp_dir.path().to_path_buf();
let fs = FileSystemManager::new();
let package_json = serde_json::json!({
"name": "test-package",
"version": "1.0.0"
});
fs.write_file_string(
&workspace_root.join("package.json"),
&serde_json::to_string_pretty(&package_json).expect("Failed to serialize"),
)
.await
.expect("Failed to write package.json");
fs.create_dir_all(&workspace_root.join("src")).await.expect("Failed to create src dir");
fs.write_file_string(&workspace_root.join("src/index.ts"), "// test")
.await
.expect("Failed to write index.ts");
(temp_dir, workspace_root)
}
async fn create_monorepo_workspace() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let workspace_root = temp_dir.path().to_path_buf();
let fs = FileSystemManager::new();
let root_package_json = serde_json::json!({
"name": "monorepo-root",
"version": "1.0.0",
"workspaces": ["packages/*"]
});
fs.write_file_string(
&workspace_root.join("package.json"),
&serde_json::to_string_pretty(&root_package_json).expect("Failed to serialize"),
)
.await
.expect("Failed to write root package.json");
fs.write_file_string(&workspace_root.join("package-lock.json"), "{}")
.await
.expect("Failed to write package-lock.json");
let packages_dir = workspace_root.join("packages");
fs.create_dir_all(&packages_dir).await.expect("Failed to create packages dir");
let pkg1_dir = packages_dir.join("core");
fs.create_dir_all(&pkg1_dir).await.expect("Failed to create core package dir");
let pkg1_json = serde_json::json!({
"name": "@test/core",
"version": "1.0.0"
});
fs.write_file_string(
&pkg1_dir.join("package.json"),
&serde_json::to_string_pretty(&pkg1_json).expect("Failed to serialize"),
)
.await
.expect("Failed to write core package.json");
fs.create_dir_all(&pkg1_dir.join("src")).await.expect("Failed to create core src dir");
fs.write_file_string(&pkg1_dir.join("src/index.ts"), "// core")
.await
.expect("Failed to write core index.ts");
let pkg2_dir = packages_dir.join("utils");
fs.create_dir_all(&pkg2_dir).await.expect("Failed to create utils package dir");
let pkg2_json = serde_json::json!({
"name": "@test/utils",
"version": "1.0.0"
});
fs.write_file_string(
&pkg2_dir.join("package.json"),
&serde_json::to_string_pretty(&pkg2_json).expect("Failed to serialize"),
)
.await
.expect("Failed to write utils package.json");
fs.create_dir_all(&pkg2_dir.join("src")).await.expect("Failed to create utils src dir");
fs.write_file_string(&pkg2_dir.join("src/helper.ts"), "// utils")
.await
.expect("Failed to write utils helper.ts");
(temp_dir, workspace_root)
}
#[tokio::test]
async fn test_single_package_map_files() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let files = vec![PathBuf::from("src/index.ts"), PathBuf::from("package.json")];
let result = mapper.map_files_to_packages(&files).await;
assert!(result.is_ok());
let package_files = result.expect("Expected Ok result");
assert_eq!(package_files.len(), 1);
assert!(package_files.contains_key("test-package"));
assert_eq!(package_files["test-package"].len(), 2);
}
#[tokio::test]
async fn test_single_package_find_package_for_file() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let file = PathBuf::from("src/index.ts");
let result = mapper.find_package_for_file(&file).await;
assert!(result.is_ok());
let package_name = result.expect("Expected Ok result");
assert_eq!(package_name, Some("test-package".to_string()));
}
#[tokio::test]
async fn test_single_package_get_all_packages() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let result = mapper.get_all_packages().await;
assert!(result.is_ok());
let packages = result.expect("Expected Ok result");
assert_eq!(packages.len(), 1);
assert_eq!(packages[0].name(), "test-package");
}
#[tokio::test]
async fn test_single_package_is_monorepo() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let result = mapper.is_monorepo().await;
assert!(result.is_ok());
assert!(!result.expect("Expected Ok result"));
}
#[tokio::test]
async fn test_monorepo_map_files() {
let (_temp, workspace_root) = create_monorepo_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let files = vec![
PathBuf::from("packages/core/src/index.ts"),
PathBuf::from("packages/utils/src/helper.ts"),
PathBuf::from("README.md"), ];
let result = mapper.map_files_to_packages(&files).await;
assert!(result.is_ok());
let package_files = result.expect("Expected Ok result");
assert_eq!(package_files.len(), 2);
assert!(package_files.contains_key("@test/core"));
assert!(package_files.contains_key("@test/utils"));
assert_eq!(package_files["@test/core"].len(), 1);
assert_eq!(package_files["@test/utils"].len(), 1);
}
#[tokio::test]
async fn test_monorepo_find_package_for_file() {
let (_temp, workspace_root) = create_monorepo_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let file1 = PathBuf::from("packages/core/src/index.ts");
let result1 = mapper.find_package_for_file(&file1).await;
assert!(result1.is_ok());
assert_eq!(result1.expect("Expected Ok"), Some("@test/core".to_string()));
let file2 = PathBuf::from("packages/utils/src/helper.ts");
let result2 = mapper.find_package_for_file(&file2).await;
assert!(result2.is_ok());
assert_eq!(result2.expect("Expected Ok"), Some("@test/utils".to_string()));
let file3 = PathBuf::from("README.md");
let result3 = mapper.find_package_for_file(&file3).await;
assert!(result3.is_ok());
assert_eq!(result3.expect("Expected Ok"), None);
}
#[tokio::test]
async fn test_monorepo_get_all_packages() {
let (_temp, workspace_root) = create_monorepo_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let result = mapper.get_all_packages().await;
assert!(result.is_ok());
let packages = result.expect("Expected Ok result");
assert_eq!(packages.len(), 2);
let package_names: Vec<_> = packages.iter().map(|p| p.name()).collect();
assert!(package_names.contains(&"@test/core"));
assert!(package_names.contains(&"@test/utils"));
}
#[tokio::test]
async fn test_monorepo_is_monorepo() {
let (_temp, workspace_root) = create_monorepo_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let result = mapper.is_monorepo().await;
assert!(result.is_ok());
assert!(result.expect("Expected Ok result"));
}
#[tokio::test]
async fn test_cache_behavior() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let file = PathBuf::from("src/index.ts");
let result1 = mapper.find_package_for_file(&file).await;
assert!(result1.is_ok());
assert!(mapper.file_cache.contains_key(&file));
let result2 = mapper.find_package_for_file(&file).await;
assert!(result2.is_ok());
assert_eq!(result1.unwrap(), result2.unwrap());
mapper.clear_cache();
assert!(mapper.file_cache.is_empty());
assert!(mapper.cached_monorepo.is_none());
}
#[tokio::test]
async fn test_normalize_path_relative() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mapper = PackageMapper::new(workspace_root.clone(), fs);
let relative_path = PathBuf::from("src/index.ts");
let result = mapper.normalize_path(&relative_path);
assert!(result.is_ok());
assert_eq!(result.expect("Expected Ok"), relative_path);
}
#[tokio::test]
async fn test_normalize_path_absolute() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mapper = PackageMapper::new(workspace_root.clone(), fs);
let absolute_path = workspace_root.join("src/index.ts");
let result = mapper.normalize_path(&absolute_path);
assert!(result.is_ok());
assert_eq!(result.expect("Expected Ok"), PathBuf::from("src/index.ts"));
}
#[tokio::test]
async fn test_normalize_path_outside_workspace() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mapper = PackageMapper::new(workspace_root.clone(), fs);
let outside_temp = TempDir::new().expect("Failed to create outside temp dir");
let outside_path = outside_temp.path().join("outside/file.ts");
let result = mapper.normalize_path(&outside_path);
assert!(result.is_err());
if let Err(ChangesError::FileOutsideWorkspace { .. }) = result {
} else {
panic!("Expected FileOutsideWorkspace error");
}
}
#[tokio::test]
async fn test_empty_file_list() {
let (_temp, workspace_root) = create_single_package_workspace().await;
let fs = FileSystemManager::new();
let mut mapper = PackageMapper::new(workspace_root.clone(), fs);
let files: Vec<PathBuf> = vec![];
let result = mapper.map_files_to_packages(&files).await;
assert!(result.is_ok());
let package_files = result.expect("Expected Ok result");
assert!(package_files.is_empty());
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod working_directory_tests {
use crate::changes::{AnalysisMode, ChangesAnalyzer, FileChangeType};
use crate::config::PackageToolsConfig;
use std::path::PathBuf;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::FileSystemManager;
use tempfile::TempDir;
use tokio::fs;
async fn create_test_workspace_with_git() -> (TempDir, PathBuf) {
let temp = TempDir::new().expect("Failed to create temp dir");
let workspace_root = temp.path().to_path_buf();
let package_json = serde_json::json!({
"name": "@test/package",
"version": "1.0.0"
});
fs::write(
workspace_root.join("package.json"),
serde_json::to_string_pretty(&package_json).expect("Failed to serialize JSON"),
)
.await
.expect("Failed to write package.json");
let repo = Repo::create(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to create git repo");
repo.config("user.name", "Test User").expect("Failed to set git user.name");
repo.config("user.email", "test@example.com").expect("Failed to set git user.email");
repo.add_all().expect("Failed to add files");
repo.commit("Initial commit").expect("Failed to create initial commit");
(temp, workspace_root)
}
async fn create_test_monorepo_with_git() -> (TempDir, PathBuf) {
let temp = TempDir::new().expect("Failed to create temp dir");
let workspace_root = temp.path().to_path_buf();
let root_package = serde_json::json!({
"name": "test-monorepo",
"version": "0.0.0",
"private": true,
"workspaces": ["packages/*"]
});
fs::write(
workspace_root.join("package.json"),
serde_json::to_string_pretty(&root_package).expect("Failed to serialize JSON"),
)
.await
.expect("Failed to write root package.json");
fs::create_dir_all(workspace_root.join("packages"))
.await
.expect("Failed to create packages dir");
fs::create_dir_all(workspace_root.join("packages/a"))
.await
.expect("Failed to create package a dir");
let package_a = serde_json::json!({
"name": "@test/a",
"version": "1.0.0"
});
fs::write(
workspace_root.join("packages/a/package.json"),
serde_json::to_string_pretty(&package_a).expect("Failed to serialize JSON"),
)
.await
.expect("Failed to write package a");
fs::create_dir_all(workspace_root.join("packages/b"))
.await
.expect("Failed to create package b dir");
let package_b = serde_json::json!({
"name": "@test/b",
"version": "2.0.0"
});
fs::write(
workspace_root.join("packages/b/package.json"),
serde_json::to_string_pretty(&package_b).expect("Failed to serialize JSON"),
)
.await
.expect("Failed to write package b");
let repo = Repo::create(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to create git repo");
repo.config("user.name", "Test User").expect("Failed to set git user.name");
repo.config("user.email", "test@example.com").expect("Failed to set git user.email");
repo.add_all().expect("Failed to add files");
repo.commit("Initial commit").expect("Failed to create initial commit");
(temp, workspace_root)
}
#[tokio::test]
async fn test_analyze_working_directory_no_changes() {
let (_temp, workspace_root) = create_test_workspace_with_git().await;
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
assert_eq!(report.analysis_mode, AnalysisMode::WorkingDirectory);
assert!(!report.has_changes());
assert_eq!(report.summary.packages_with_changes, 0);
assert_eq!(report.summary.total_files_changed, 0);
}
#[tokio::test]
async fn test_analyze_working_directory_with_staged_changes() {
let (_temp, workspace_root) = create_test_workspace_with_git().await;
fs::write(workspace_root.join("new-file.txt"), "test content")
.await
.expect("Failed to write new file");
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
repo.add("new-file.txt").expect("Failed to stage file");
let fs_manager = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs_manager, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
assert_eq!(report.analysis_mode, AnalysisMode::WorkingDirectory);
assert!(report.has_changes());
assert_eq!(report.summary.packages_with_changes, 1);
assert_eq!(report.summary.total_files_changed, 1);
let packages_with_changes = report.packages_with_changes();
assert_eq!(packages_with_changes.len(), 1);
let package = packages_with_changes[0];
assert_eq!(package.files.len(), 1);
assert_eq!(package.files[0].change_type, FileChangeType::Added);
}
#[tokio::test]
async fn test_analyze_working_directory_with_unstaged_changes() {
let (_temp, workspace_root) = create_test_workspace_with_git().await;
let package_json = serde_json::json!({
"name": "@test/package",
"version": "1.1.0"
});
fs::write(
workspace_root.join("package.json"),
serde_json::to_string_pretty(&package_json).expect("Failed to serialize JSON"),
)
.await
.expect("Failed to write package.json");
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
let fs_manager = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs_manager, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
assert!(report.has_changes());
assert_eq!(report.summary.total_files_changed, 1);
let package = &report.packages_with_changes()[0];
assert!(package.package_json_modified());
assert_eq!(package.files[0].change_type, FileChangeType::Modified);
}
#[tokio::test]
async fn test_analyze_working_directory_with_both_staged_and_unstaged() {
let (_temp, workspace_root) = create_test_workspace_with_git().await;
fs::write(workspace_root.join("staged.txt"), "staged")
.await
.expect("Failed to write staged file");
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
repo.add("staged.txt").expect("Failed to stage file");
fs::write(workspace_root.join("unstaged.txt"), "unstaged")
.await
.expect("Failed to write unstaged file");
let fs_manager = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs_manager, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
assert!(report.has_changes());
assert_eq!(report.summary.total_files_changed, 2);
let package = &report.packages_with_changes()[0];
assert_eq!(package.files.len(), 2);
let added_count = package.files.iter().filter(|f| f.is_addition()).count();
assert_eq!(added_count, 2);
}
#[tokio::test]
async fn test_analyze_working_directory_monorepo() {
let (_temp, workspace_root) = create_test_monorepo_with_git().await;
fs::write(workspace_root.join("packages/a/index.js"), "console.log('package a');")
.await
.expect("Failed to write file in package a");
fs::write(workspace_root.join("packages/b/index.js"), "console.log('package b');")
.await
.expect("Failed to write file in package b");
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
let fs_manager = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs_manager, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
assert!(report.has_changes());
let packages_with_changes = report.packages_with_changes();
assert!(
!packages_with_changes.is_empty(),
"Should detect at least one package with changes"
);
assert!(report.summary.total_files_changed >= 2, "Should detect at least 2 changed files");
}
#[tokio::test]
async fn test_analyze_working_directory_report_accuracy() {
let (_temp, workspace_root) = create_test_workspace_with_git().await;
fs::write(workspace_root.join("file1.txt"), "content 1")
.await
.expect("Failed to write file1");
fs::write(workspace_root.join("file2.txt"), "content 2")
.await
.expect("Failed to write file2");
fs::write(workspace_root.join("file3.txt"), "content 3")
.await
.expect("Failed to write file3");
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
let fs_manager = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs_manager, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
assert_eq!(report.summary.total_files_changed, 3);
assert_eq!(report.summary.packages_with_changes, 1);
assert_eq!(report.summary.total_packages, 1);
assert_eq!(report.summary.packages_without_changes, 0);
let package = &report.packages_with_changes()[0];
assert_eq!(package.stats.files_changed, 3);
assert_eq!(package.stats.files_added, 3);
assert_eq!(package.stats.files_modified, 0);
assert_eq!(package.stats.files_deleted, 0);
}
#[tokio::test]
async fn test_analyze_working_directory_deleted_files() {
let (_temp, workspace_root) = create_test_workspace_with_git().await;
fs::write(workspace_root.join("to-delete.txt"), "content")
.await
.expect("Failed to write file");
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
repo.add("to-delete.txt").expect("Failed to add file");
repo.commit("Add file to delete").expect("Failed to commit");
fs::remove_file(workspace_root.join("to-delete.txt")).await.expect("Failed to delete file");
let fs_manager = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs_manager, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
assert!(report.has_changes());
let package = &report.packages_with_changes()[0];
assert_eq!(package.stats.files_deleted, 1);
assert_eq!(package.files[0].change_type, FileChangeType::Deleted);
}
#[tokio::test]
async fn test_working_directory_with_current_version() {
let (_temp, workspace_root) = create_test_workspace_with_git().await;
fs::write(workspace_root.join("new.txt"), "new content")
.await
.expect("Failed to write file");
let repo = Repo::open(workspace_root.to_str().expect("Invalid path"))
.expect("Failed to open repo");
let fs_manager = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer = ChangesAnalyzer::with_filesystem(workspace_root, repo, fs_manager, config)
.await
.expect("Failed to create analyzer");
let report = analyzer
.analyze_working_directory()
.await
.expect("Failed to analyze working directory");
let package = &report.packages_with_changes()[0];
assert!(package.current_version.is_some());
if let Some(version) = &package.current_version {
assert_eq!(version.to_string(), "1.0.0");
}
assert!(package.next_version.is_none());
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod version_preview_tests {
use crate::changes::ChangesAnalyzer;
use crate::config::PackageToolsConfig;
use crate::types::{Changeset, Version, VersionBump};
use std::fs;
use std::path::Path;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::FileSystemManager;
fn create_test_repo_with_commits_for_versions(
temp_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let repo = Repo::create(temp_dir.to_str().unwrap())?;
repo.config("Test User", "test@example.com")?;
let package_json = serde_json::json!({
"name": "@myorg/core",
"version": "1.2.3",
});
let package_json_path = temp_dir.join("package.json");
fs::write(&package_json_path, serde_json::to_string_pretty(&package_json)?)?;
let src_dir = temp_dir.join("src");
fs::create_dir_all(&src_dir)?;
fs::write(src_dir.join("index.ts"), "export const version = '1.2.3';")?;
repo.add_all()?;
repo.commit("Initial commit")?;
fs::write(src_dir.join("index.ts"), "export const version = '1.2.4';")?;
repo.add_all()?;
repo.commit("Update version")?;
Ok(())
}
fn create_monorepo_for_versions(temp_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let repo = Repo::create(temp_dir.to_str().unwrap())?;
repo.config("Test User", "test@example.com")?;
let root_package_json = serde_json::json!({
"name": "monorepo-root",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"],
});
fs::write(
temp_dir.join("package.json"),
serde_json::to_string_pretty(&root_package_json)?,
)?;
let pnpm_workspace = "packages:\n - 'packages/*'\n";
fs::write(temp_dir.join("pnpm-workspace.yaml"), pnpm_workspace)?;
let packages_dir = temp_dir.join("packages");
fs::create_dir_all(&packages_dir)?;
let core_dir = packages_dir.join("core");
fs::create_dir_all(&core_dir)?;
let core_package_json = serde_json::json!({
"name": "@myorg/core",
"version": "2.0.0",
});
fs::write(
core_dir.join("package.json"),
serde_json::to_string_pretty(&core_package_json)?,
)?;
fs::write(core_dir.join("index.ts"), "export const core = true;")?;
let utils_dir = packages_dir.join("utils");
fs::create_dir_all(&utils_dir)?;
let utils_package_json = serde_json::json!({
"name": "@myorg/utils",
"version": "0.5.0",
});
fs::write(
utils_dir.join("package.json"),
serde_json::to_string_pretty(&utils_package_json)?,
)?;
fs::write(utils_dir.join("index.ts"), "export const utils = true;")?;
repo.add_all()?;
repo.commit("Initial monorepo setup")?;
fs::write(core_dir.join("index.ts"), "export const core = true; // updated")?;
repo.add_all()?;
repo.commit("Update core package")?;
Ok(())
}
#[tokio::test]
async fn test_analyze_with_versions_patch_bump() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_test_repo_with_commits_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let mut changeset =
Changeset::new("feature-branch", VersionBump::Patch, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let package = report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(package.current_version.as_ref().unwrap().to_string(), "1.2.3");
assert_eq!(package.next_version.as_ref().unwrap().to_string(), "1.2.4");
assert_eq!(package.bump_type, Some(VersionBump::Patch));
}
#[tokio::test]
async fn test_analyze_with_versions_minor_bump() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_test_repo_with_commits_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let mut changeset =
Changeset::new("feature-branch", VersionBump::Minor, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let package = report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(package.current_version.as_ref().unwrap().to_string(), "1.2.3");
assert_eq!(package.next_version.as_ref().unwrap().to_string(), "1.3.0");
assert_eq!(package.bump_type, Some(VersionBump::Minor));
}
#[tokio::test]
async fn test_analyze_with_versions_major_bump() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_test_repo_with_commits_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let mut changeset =
Changeset::new("feature-branch", VersionBump::Major, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let package = report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(package.current_version.as_ref().unwrap().to_string(), "1.2.3");
assert_eq!(package.next_version.as_ref().unwrap().to_string(), "2.0.0");
assert_eq!(package.bump_type, Some(VersionBump::Major));
}
#[tokio::test]
async fn test_analyze_with_versions_no_bump() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_test_repo_with_commits_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let mut changeset =
Changeset::new("feature-branch", VersionBump::None, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let package = report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(package.current_version.as_ref().unwrap().to_string(), "1.2.3");
assert_eq!(package.next_version.as_ref().unwrap().to_string(), "1.2.3");
assert_eq!(package.bump_type, Some(VersionBump::None));
}
#[tokio::test]
async fn test_analyze_with_versions_monorepo() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_monorepo_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let mut changeset =
Changeset::new("feature-branch", VersionBump::Minor, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let core_package =
report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(core_package.current_version.as_ref().unwrap().to_string(), "2.0.0");
assert_eq!(core_package.next_version.as_ref().unwrap().to_string(), "2.1.0");
assert_eq!(core_package.bump_type, Some(VersionBump::Minor));
let utils_package =
report.packages.iter().find(|p| p.package_name() == "@myorg/utils").unwrap();
assert_eq!(utils_package.current_version.as_ref().unwrap().to_string(), "0.5.0");
assert!(utils_package.next_version.is_none());
assert!(utils_package.bump_type.is_none());
}
#[tokio::test]
async fn test_analyze_with_versions_multiple_packages() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_monorepo_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let mut changeset =
Changeset::new("feature-branch", VersionBump::Patch, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
changeset.packages.push("@myorg/utils".to_string());
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let core_package =
report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(core_package.next_version.as_ref().unwrap().to_string(), "2.0.1");
assert_eq!(core_package.bump_type, Some(VersionBump::Patch));
let utils_package =
report.packages.iter().find(|p| p.package_name() == "@myorg/utils").unwrap();
assert_eq!(utils_package.next_version.as_ref().unwrap().to_string(), "0.5.1");
assert_eq!(utils_package.bump_type, Some(VersionBump::Patch));
}
#[tokio::test]
async fn test_analyze_with_versions_prerelease_versions() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
let repo = Repo::create(workspace_root.to_str().unwrap()).unwrap();
repo.config("Test User", "test@example.com").unwrap();
let package_json = serde_json::json!({
"name": "@myorg/core",
"version": "1.0.0-beta.1",
});
let package_json_path = workspace_root.join("package.json");
fs::write(&package_json_path, serde_json::to_string_pretty(&package_json).unwrap())
.unwrap();
let src_dir = workspace_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("index.ts"), "export const v = 1;").unwrap();
repo.add_all().unwrap();
repo.commit("Initial commit").unwrap();
fs::write(src_dir.join("index.ts"), "export const v = 2;").unwrap();
repo.add_all().unwrap();
repo.commit("Update").unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let mut changeset =
Changeset::new("feature-branch", VersionBump::Patch, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let package = report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(package.current_version.as_ref().unwrap().to_string(), "1.0.0-beta.1");
assert_eq!(package.next_version.as_ref().unwrap().to_string(), "1.0.1");
}
#[tokio::test]
async fn test_analyze_with_versions_empty_changeset_packages() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_test_repo_with_commits_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let changeset =
Changeset::new("feature-branch", VersionBump::Minor, vec!["production".to_string()]);
let report = analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let package = report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert!(package.current_version.is_some());
assert!(package.next_version.is_none());
assert!(package.bump_type.is_none());
}
#[tokio::test]
async fn test_analyze_with_versions_consistency_with_version_resolver() {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path().to_path_buf();
create_test_repo_with_commits_for_versions(&workspace_root).unwrap();
let git_repo = Repo::open(workspace_root.to_str().unwrap()).unwrap();
let fs = FileSystemManager::new();
let config = PackageToolsConfig::default();
let analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs, config).await.unwrap();
let test_cases = vec![
(VersionBump::Major, "2.0.0"),
(VersionBump::Minor, "1.3.0"),
(VersionBump::Patch, "1.2.4"),
(VersionBump::None, "1.2.3"),
];
for (bump_type, expected_version) in test_cases {
let mut changeset =
Changeset::new("feature-branch", bump_type, vec!["production".to_string()]);
changeset.packages.push("@myorg/core".to_string());
let report =
analyzer.analyze_with_versions("HEAD~1", "HEAD", &changeset).await.unwrap();
let package =
report.packages.iter().find(|p| p.package_name() == "@myorg/core").unwrap();
assert_eq!(
package.next_version.as_ref().unwrap().to_string(),
expected_version,
"Failed for bump type {:?}",
bump_type
);
let current = Version::parse("1.2.3").unwrap();
let expected = current.bump(bump_type).unwrap();
assert_eq!(
package.next_version.as_ref().unwrap(),
&expected,
"Version calculation inconsistent with Version::bump for {:?}",
bump_type
);
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod stats_tests {
use crate::changes::{
ChangesSummary, FileChange, FileChangeType, PackageChangeStats, PackageChanges,
};
use std::path::PathBuf;
use sublime_standard_tools::monorepo::WorkspacePackage;
fn create_test_workspace_package(name: &str) -> WorkspacePackage {
WorkspacePackage {
name: name.to_string(),
version: "1.0.0".to_string(),
location: PathBuf::from(format!("packages/{}", name)),
absolute_path: PathBuf::from(format!("/workspace/packages/{}", name)),
workspace_dependencies: Vec::new(),
workspace_dev_dependencies: Vec::new(),
}
}
#[test]
fn test_package_change_stats_new() {
let stats = PackageChangeStats::new();
assert_eq!(stats.files_changed, 0);
assert_eq!(stats.files_added, 0);
assert_eq!(stats.files_modified, 0);
assert_eq!(stats.files_deleted, 0);
assert_eq!(stats.commits, 0);
assert_eq!(stats.lines_added, 0);
assert_eq!(stats.lines_deleted, 0);
assert!(!stats.has_changes());
}
#[test]
fn test_package_change_stats_has_changes() {
let mut stats = PackageChangeStats::new();
assert!(!stats.has_changes());
stats.files_changed = 1;
assert!(stats.has_changes());
}
#[test]
fn test_package_change_stats_net_lines_changed() {
let stats = PackageChangeStats {
files_changed: 5,
files_added: 2,
files_modified: 3,
files_deleted: 0,
commits: 3,
lines_added: 150,
lines_deleted: 30,
};
assert_eq!(stats.net_lines_changed(), 120);
}
#[test]
fn test_package_change_stats_net_lines_negative() {
let stats = PackageChangeStats {
files_changed: 5,
files_added: 0,
files_modified: 3,
files_deleted: 2,
commits: 3,
lines_added: 30,
lines_deleted: 150,
};
assert_eq!(stats.net_lines_changed(), -120);
}
#[test]
fn test_package_change_stats_total_lines_changed() {
let stats = PackageChangeStats {
files_changed: 5,
files_added: 2,
files_modified: 3,
files_deleted: 0,
commits: 3,
lines_added: 150,
lines_deleted: 30,
};
assert_eq!(stats.total_lines_changed(), 180);
}
#[test]
fn test_package_change_stats_average_lines_per_file() {
let stats = PackageChangeStats {
files_changed: 5,
files_added: 2,
files_modified: 3,
files_deleted: 0,
commits: 3,
lines_added: 150,
lines_deleted: 30,
};
assert_eq!(stats.average_lines_per_file(), 36.0);
}
#[test]
fn test_package_change_stats_average_lines_zero_files() {
let stats = PackageChangeStats::new();
assert_eq!(stats.average_lines_per_file(), 0.0);
}
#[test]
fn test_package_change_stats_percentages() {
let stats = PackageChangeStats {
files_changed: 10,
files_added: 2,
files_modified: 6,
files_deleted: 2,
commits: 5,
lines_added: 100,
lines_deleted: 20,
};
assert_eq!(stats.added_percentage(), 20.0);
assert_eq!(stats.modified_percentage(), 60.0);
assert_eq!(stats.deleted_percentage(), 20.0);
}
#[test]
fn test_package_change_stats_percentages_zero_files() {
let stats = PackageChangeStats::new();
assert_eq!(stats.added_percentage(), 0.0);
assert_eq!(stats.modified_percentage(), 0.0);
assert_eq!(stats.deleted_percentage(), 0.0);
}
#[test]
fn test_changes_summary_new() {
let summary = ChangesSummary::new();
assert_eq!(summary.total_packages, 0);
assert_eq!(summary.packages_with_changes, 0);
assert_eq!(summary.packages_without_changes, 0);
assert_eq!(summary.total_files_changed, 0);
assert_eq!(summary.total_commits, 0);
assert_eq!(summary.total_lines_added, 0);
assert_eq!(summary.total_lines_deleted, 0);
assert!(!summary.has_changes());
}
#[test]
fn test_changes_summary_has_changes() {
let mut summary = ChangesSummary::new();
assert!(!summary.has_changes());
summary.packages_with_changes = 1;
assert!(summary.has_changes());
summary.packages_with_changes = 0;
summary.total_files_changed = 1;
assert!(summary.has_changes());
}
#[test]
fn test_changes_summary_change_percentage() {
let summary = ChangesSummary {
total_packages: 10,
packages_with_changes: 3,
packages_without_changes: 7,
total_files_changed: 15,
total_commits: 5,
total_lines_added: 100,
total_lines_deleted: 20,
};
assert_eq!(summary.change_percentage(), 30.0);
}
#[test]
fn test_changes_summary_change_percentage_zero_packages() {
let summary = ChangesSummary::new();
assert_eq!(summary.change_percentage(), 0.0);
}
#[test]
fn test_changes_summary_net_lines_changed() {
let summary = ChangesSummary {
total_packages: 5,
packages_with_changes: 2,
packages_without_changes: 3,
total_files_changed: 10,
total_commits: 4,
total_lines_added: 100,
total_lines_deleted: 20,
};
assert_eq!(summary.net_lines_changed(), 80);
}
#[test]
fn test_changes_summary_total_lines_changed() {
let summary = ChangesSummary {
total_packages: 5,
packages_with_changes: 2,
packages_without_changes: 3,
total_files_changed: 10,
total_commits: 4,
total_lines_added: 100,
total_lines_deleted: 20,
};
assert_eq!(summary.total_lines_changed(), 120);
}
#[test]
fn test_changes_summary_average_files_per_package() {
let summary = ChangesSummary {
total_packages: 5,
packages_with_changes: 2,
packages_without_changes: 3,
total_files_changed: 10,
total_commits: 4,
total_lines_added: 100,
total_lines_deleted: 20,
};
assert_eq!(summary.average_files_per_package(), 5.0);
}
#[test]
fn test_changes_summary_average_files_zero_packages() {
let summary = ChangesSummary::new();
assert_eq!(summary.average_files_per_package(), 0.0);
}
#[test]
fn test_package_changes_add_file_updates_stats() {
let workspace_pkg = create_test_workspace_package("core");
let mut changes = PackageChanges::new(workspace_pkg);
assert_eq!(changes.stats.files_changed, 0);
assert!(!changes.has_changes);
changes.add_file(FileChange {
path: PathBuf::from("packages/core/src/new.ts"),
package_relative_path: PathBuf::from("src/new.ts"),
change_type: FileChangeType::Added,
lines_added: Some(50),
lines_deleted: Some(0),
commits: Vec::new(),
});
assert_eq!(changes.stats.files_changed, 1);
assert_eq!(changes.stats.files_added, 1);
assert_eq!(changes.stats.files_modified, 0);
assert_eq!(changes.stats.files_deleted, 0);
assert_eq!(changes.stats.lines_added, 50);
assert_eq!(changes.stats.lines_deleted, 0);
assert!(changes.has_changes);
changes.add_file(FileChange {
path: PathBuf::from("packages/core/src/index.ts"),
package_relative_path: PathBuf::from("src/index.ts"),
change_type: FileChangeType::Modified,
lines_added: Some(30),
lines_deleted: Some(10),
commits: Vec::new(),
});
assert_eq!(changes.stats.files_changed, 2);
assert_eq!(changes.stats.files_added, 1);
assert_eq!(changes.stats.files_modified, 1);
assert_eq!(changes.stats.files_deleted, 0);
assert_eq!(changes.stats.lines_added, 80);
assert_eq!(changes.stats.lines_deleted, 10);
changes.add_file(FileChange {
path: PathBuf::from("packages/core/src/old.ts"),
package_relative_path: PathBuf::from("src/old.ts"),
change_type: FileChangeType::Deleted,
lines_added: Some(0),
lines_deleted: Some(20),
commits: Vec::new(),
});
assert_eq!(changes.stats.files_changed, 3);
assert_eq!(changes.stats.files_added, 1);
assert_eq!(changes.stats.files_modified, 1);
assert_eq!(changes.stats.files_deleted, 1);
assert_eq!(changes.stats.lines_added, 80);
assert_eq!(changes.stats.lines_deleted, 30);
}
#[test]
fn test_package_changes_add_file_with_no_line_info() {
let workspace_pkg = create_test_workspace_package("core");
let mut changes = PackageChanges::new(workspace_pkg);
changes.add_file(FileChange {
path: PathBuf::from("packages/core/src/binary.dat"),
package_relative_path: PathBuf::from("src/binary.dat"),
change_type: FileChangeType::Added,
lines_added: None,
lines_deleted: None,
commits: Vec::new(),
});
assert_eq!(changes.stats.files_changed, 1);
assert_eq!(changes.stats.files_added, 1);
assert_eq!(changes.stats.lines_added, 0);
assert_eq!(changes.stats.lines_deleted, 0);
}
#[test]
fn test_package_changes_add_file_renamed() {
let workspace_pkg = create_test_workspace_package("core");
let mut changes = PackageChanges::new(workspace_pkg);
changes.add_file(FileChange {
path: PathBuf::from("packages/core/src/renamed.ts"),
package_relative_path: PathBuf::from("src/renamed.ts"),
change_type: FileChangeType::Renamed,
lines_added: Some(5),
lines_deleted: Some(3),
commits: Vec::new(),
});
assert_eq!(changes.stats.files_changed, 1);
assert_eq!(changes.stats.files_added, 0);
assert_eq!(changes.stats.files_modified, 1);
assert_eq!(changes.stats.files_deleted, 0);
assert_eq!(changes.stats.lines_added, 5);
assert_eq!(changes.stats.lines_deleted, 3);
}
#[test]
fn test_package_changes_add_file_copied() {
let workspace_pkg = create_test_workspace_package("core");
let mut changes = PackageChanges::new(workspace_pkg);
changes.add_file(FileChange {
path: PathBuf::from("packages/core/src/copied.ts"),
package_relative_path: PathBuf::from("src/copied.ts"),
change_type: FileChangeType::Copied,
lines_added: Some(50),
lines_deleted: Some(0),
commits: Vec::new(),
});
assert_eq!(changes.stats.files_changed, 1);
assert_eq!(changes.stats.files_added, 1);
assert_eq!(changes.stats.files_modified, 0);
assert_eq!(changes.stats.files_deleted, 0);
}
#[test]
fn test_package_changes_add_file_untracked() {
let workspace_pkg = create_test_workspace_package("core");
let mut changes = PackageChanges::new(workspace_pkg);
changes.add_file(FileChange {
path: PathBuf::from("packages/core/src/untracked.ts"),
package_relative_path: PathBuf::from("src/untracked.ts"),
change_type: FileChangeType::Untracked,
lines_added: Some(25),
lines_deleted: Some(0),
commits: Vec::new(),
});
assert_eq!(changes.stats.files_changed, 1);
assert_eq!(changes.stats.files_added, 1);
assert_eq!(changes.stats.files_modified, 0);
assert_eq!(changes.stats.files_deleted, 0);
}
#[test]
fn test_package_changes_add_commit_updates_stats() {
use crate::changes::CommitInfo;
use chrono::Utc;
let workspace_pkg = create_test_workspace_package("core");
let mut changes = PackageChanges::new(workspace_pkg);
assert_eq!(changes.stats.commits, 0);
changes.add_commit(CommitInfo {
hash: "abc123".to_string(),
short_hash: "abc123".to_string(),
author: "John Doe".to_string(),
author_email: "john@example.com".to_string(),
date: Utc::now(),
message: "feat: add feature".to_string(),
full_message: "feat: add feature\n\nDetailed description".to_string(),
affected_packages: vec!["core".to_string()],
files_changed: 1,
lines_added: 10,
lines_deleted: 2,
});
assert_eq!(changes.stats.commits, 1);
assert_eq!(changes.commits.len(), 1);
changes.add_commit(CommitInfo {
hash: "def456".to_string(),
short_hash: "def456".to_string(),
author: "Jane Doe".to_string(),
author_email: "jane@example.com".to_string(),
date: Utc::now(),
message: "fix: bug fix".to_string(),
full_message: "fix: bug fix".to_string(),
affected_packages: vec!["core".to_string()],
files_changed: 2,
lines_added: 5,
lines_deleted: 8,
});
assert_eq!(changes.stats.commits, 2);
assert_eq!(changes.commits.len(), 2);
}
#[test]
fn test_stats_calculations_with_various_changes() {
let stats = PackageChangeStats {
files_changed: 15,
files_added: 5,
files_modified: 8,
files_deleted: 2,
commits: 10,
lines_added: 500,
lines_deleted: 200,
};
assert_eq!(stats.files_changed, 15);
assert_eq!(stats.files_added, 5);
assert_eq!(stats.files_modified, 8);
assert_eq!(stats.files_deleted, 2);
assert_eq!(stats.commits, 10);
assert_eq!(stats.lines_added, 500);
assert_eq!(stats.lines_deleted, 200);
assert!(stats.has_changes());
assert_eq!(stats.net_lines_changed(), 300);
assert_eq!(stats.total_lines_changed(), 700);
assert!((stats.average_lines_per_file() - 46.666).abs() < 0.01);
assert!((stats.added_percentage() - 33.333).abs() < 0.01);
assert!((stats.modified_percentage() - 53.333).abs() < 0.01);
assert!((stats.deleted_percentage() - 13.333).abs() < 0.01);
}
#[test]
fn test_stats_edge_case_all_deletions() {
let stats = PackageChangeStats {
files_changed: 5,
files_added: 0,
files_modified: 0,
files_deleted: 5,
commits: 2,
lines_added: 0,
lines_deleted: 150,
};
assert_eq!(stats.deleted_percentage(), 100.0);
assert_eq!(stats.added_percentage(), 0.0);
assert_eq!(stats.modified_percentage(), 0.0);
assert_eq!(stats.net_lines_changed(), -150);
}
#[test]
fn test_stats_edge_case_all_additions() {
let stats = PackageChangeStats {
files_changed: 5,
files_added: 5,
files_modified: 0,
files_deleted: 0,
commits: 3,
lines_added: 200,
lines_deleted: 0,
};
assert_eq!(stats.added_percentage(), 100.0);
assert_eq!(stats.modified_percentage(), 0.0);
assert_eq!(stats.deleted_percentage(), 0.0);
assert_eq!(stats.net_lines_changed(), 200);
}
#[test]
fn test_summary_edge_case_no_packages_with_changes() {
let summary = ChangesSummary {
total_packages: 10,
packages_with_changes: 0,
packages_without_changes: 10,
total_files_changed: 0,
total_commits: 0,
total_lines_added: 0,
total_lines_deleted: 0,
};
assert!(!summary.has_changes());
assert_eq!(summary.change_percentage(), 0.0);
assert_eq!(summary.net_lines_changed(), 0);
assert_eq!(summary.total_lines_changed(), 0);
assert_eq!(summary.average_files_per_package(), 0.0);
}
#[test]
fn test_summary_edge_case_all_packages_with_changes() {
let summary = ChangesSummary {
total_packages: 5,
packages_with_changes: 5,
packages_without_changes: 0,
total_files_changed: 25,
total_commits: 10,
total_lines_added: 500,
total_lines_deleted: 100,
};
assert!(summary.has_changes());
assert_eq!(summary.change_percentage(), 100.0);
assert_eq!(summary.net_lines_changed(), 400);
assert_eq!(summary.total_lines_changed(), 600);
assert_eq!(summary.average_files_per_package(), 5.0);
}
#[test]
fn test_stats_serialization() {
use serde_json;
let stats = PackageChangeStats {
files_changed: 5,
files_added: 2,
files_modified: 3,
files_deleted: 0,
commits: 3,
lines_added: 150,
lines_deleted: 30,
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("\"files_changed\":5"));
assert!(json.contains("\"files_added\":2"));
assert!(json.contains("\"lines_added\":150"));
let deserialized: PackageChangeStats = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.files_changed, 5);
assert_eq!(deserialized.files_added, 2);
assert_eq!(deserialized.lines_added, 150);
}
#[test]
fn test_summary_serialization() {
use serde_json;
let summary = ChangesSummary {
total_packages: 10,
packages_with_changes: 3,
packages_without_changes: 7,
total_files_changed: 15,
total_commits: 5,
total_lines_added: 100,
total_lines_deleted: 20,
};
let json = serde_json::to_string(&summary).unwrap();
assert!(json.contains("\"total_packages\":10"));
assert!(json.contains("\"packages_with_changes\":3"));
let deserialized: ChangesSummary = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.total_packages, 10);
assert_eq!(deserialized.packages_with_changes, 3);
}
}