monochange_cargo 0.5.1

Manage versions and releases for your multiplatform, multilanguage monorepo
Documentation
use monochange_core::AnalyzedFileChange;
use monochange_core::FileChangeKind;

use super::*;

#[test]
fn module_prefix_for_root_library_file_is_empty() {
	assert!(module_prefix_for_file(Path::new("src/lib.rs")).is_empty());
}

#[test]
fn module_prefix_for_nested_module_tracks_path_components() {
	assert_eq!(
		module_prefix_for_file(Path::new("src/api/render.rs")),
		vec!["api".to_string(), "render".to_string()]
	);
	assert_eq!(
		module_prefix_for_file(Path::new("src/api/mod.rs")),
		vec!["api".to_string()]
	);
}

#[test]
fn analyze_manifest_change_reports_dependency_and_feature_diffs() {
	let change = AnalyzedFileChange {
		path: PathBuf::from("crates/core/Cargo.toml"),
		package_path: PathBuf::from("Cargo.toml"),
		kind: FileChangeKind::Modified,
		before_contents: Some(
			"[package]\nname = \"core\"\nedition = \"2021\"\n\n[dependencies]\nserde = \"1\"\n\n[features]\ndefault = []\n"
				.to_string(),
		),
		after_contents: Some(
			"[package]\nname = \"core\"\nedition = \"2024\"\n\n[dependencies]\nserde = \"1\"\ntracing = \"0.1\"\n\n[features]\ndefault = [\"cli\"]\ncli = []\n"
				.to_string(),
		),
	};
	let mut warnings = Vec::new();
	let changes = analyze_manifest_change(&change, &mut warnings);

	assert!(warnings.is_empty());
	assert!(changes.iter().any(|change| {
		change.category == SemanticChangeCategory::Dependency
			&& change.item_path == "tracing"
			&& change.kind == SemanticChangeKind::Added
	}));
	assert!(changes.iter().any(|change| {
		change.category == SemanticChangeCategory::Metadata
			&& change.item_path == "package.edition"
			&& change.kind == SemanticChangeKind::Modified
	}));
	assert!(changes.iter().any(|change| {
		change.category == SemanticChangeCategory::Metadata
			&& change.item_path == "feature.cli"
			&& change.kind == SemanticChangeKind::Added
	}));
}

#[test]
fn collect_public_symbols_finds_public_items() {
	let file = PackageSnapshotFile {
		path: PathBuf::from("src/lib.rs"),
		contents: concat!(
			"pub struct Greeter;\n",
			"pub fn greet() {}\n",
			"pub mod api { pub fn render() {} }\n",
			"fn helper() {}\n",
		)
		.to_string(),
	};

	let symbols = collect_public_symbols(&file)
		.unwrap_or_else(|error| panic!("symbol extraction should succeed: {error}"));

	assert!(symbols.iter().any(|symbol| symbol.item_path == "Greeter"));
	assert!(symbols.iter().any(|symbol| symbol.item_path == "greet"));
	assert!(symbols.iter().any(|symbol| symbol.item_path == "api"));
	assert!(
		symbols
			.iter()
			.any(|symbol| symbol.item_path == "api::render")
	);
	assert!(!symbols.iter().any(|symbol| symbol.item_path == "helper"));
}

#[test]
fn snapshot_public_symbols_uses_changed_files_and_collects_warnings() {
	let changed_files = vec![
		AnalyzedFileChange {
			path: PathBuf::from("crates/core/src/lib.rs"),
			package_path: PathBuf::from("src/lib.rs"),
			kind: FileChangeKind::Modified,
			before_contents: None,
			after_contents: Some("pub struct Greeter;".to_string()),
		},
		AnalyzedFileChange {
			path: PathBuf::from("crates/core/src/helper.txt"),
			package_path: PathBuf::from("src/helper.txt"),
			kind: FileChangeKind::Modified,
			before_contents: None,
			after_contents: Some("ignored".to_string()),
		},
		AnalyzedFileChange {
			path: PathBuf::from("crates/core/src/bad.rs"),
			package_path: PathBuf::from("src/bad.rs"),
			kind: FileChangeKind::Modified,
			before_contents: Some("pub fn broken(".to_string()),
			after_contents: None,
		},
		AnalyzedFileChange {
			path: PathBuf::from("crates/core/src/empty.rs"),
			package_path: PathBuf::from("src/empty.rs"),
			kind: FileChangeKind::Modified,
			before_contents: None,
			after_contents: None,
		},
	];

	let (symbols, warnings) =
		snapshot_public_symbols(None, &changed_files, DetectionLevel::Signature);

	assert!(symbols.contains_key(&("struct".to_string(), "Greeter".to_string())));
	assert_eq!(warnings.len(), 1);
	assert!(
		warnings
			.first()
			.unwrap_or_else(|| panic!("expected one parse warning"))
			.contains("failed to parse src/bad.rs")
	);
}

#[test]
fn collect_public_symbols_covers_all_supported_public_item_kinds() {
	let file = PackageSnapshotFile {
		path: PathBuf::from("src/api.rs"),
		contents: concat!(
			"pub const LIMIT: usize = 3;\n",
			"pub enum Mode { Fast }\n",
			"pub static NAME: &str = \"core\";\n",
			"pub struct Greeter;\n",
			"pub trait Renderer {}\n",
			"pub type Greeting = String;\n",
			"pub union Number { value: u32 }\n",
			"pub use crate::helpers::render;\n",
		)
		.to_string(),
	};

	let symbols = collect_public_symbols(&file)
		.unwrap_or_else(|error| panic!("symbol extraction should succeed: {error}"));

	for expected in [
		"LIMIT",
		"Mode",
		"NAME",
		"Greeter",
		"Renderer",
		"Greeting",
		"Number",
		"crate :: helpers :: render",
	] {
		assert!(
			symbols
				.iter()
				.any(|symbol| symbol.item_path.ends_with(expected))
		);
	}
}

#[test]
fn module_prefix_and_symbol_diff_cover_root_removed_and_unchanged_paths() {
	assert!(module_prefix_for_file(Path::new("src/lib.rs")).is_empty());
	assert!(module_prefix_for_file(Path::new("src/main.rs")).is_empty());
	assert!(module_prefix_for_file(Path::new("lib.rs")).is_empty());

	let before = BTreeMap::from([
		(
			("function".to_string(), "greet".to_string()),
			PublicSymbol {
				item_kind: "function".to_string(),
				item_path: "greet".to_string(),
				signature: "pub fn greet()".to_string(),
				file_path: PathBuf::from("src/lib.rs"),
			},
		),
		(
			("struct".to_string(), "Greeter".to_string()),
			PublicSymbol {
				item_kind: "struct".to_string(),
				item_path: "Greeter".to_string(),
				signature: "pub struct Greeter;".to_string(),
				file_path: PathBuf::from("src/lib.rs"),
			},
		),
	]);
	let after = BTreeMap::from([
		(
			("function".to_string(), "greet".to_string()),
			PublicSymbol {
				item_kind: "function".to_string(),
				item_path: "greet".to_string(),
				signature: "pub fn greet(name: &str)".to_string(),
				file_path: PathBuf::from("src/lib.rs"),
			},
		),
		(
			("constant".to_string(), "LIMIT".to_string()),
			PublicSymbol {
				item_kind: "constant".to_string(),
				item_path: "LIMIT".to_string(),
				signature: "pub const LIMIT: usize = 3;".to_string(),
				file_path: PathBuf::from("src/lib.rs"),
			},
		),
	]);

	let changes = diff_public_symbols(&before, &after);

	assert!(
		changes
			.iter()
			.any(|change| change.kind == SemanticChangeKind::Modified)
	);
	assert!(
		changes
			.iter()
			.any(|change| change.kind == SemanticChangeKind::Removed)
	);
	assert!(changes.iter().all(|change| {
		change.summary.contains("added")
			|| change.summary.contains("modified")
			|| change.summary.contains("removed")
	}));
}

#[test]
fn snapshot_public_symbols_collects_snapshot_parse_warnings() {
	let snapshot = PackageSnapshot {
		label: "HEAD".to_string(),
		files: vec![PackageSnapshotFile {
			path: PathBuf::from("src/lib.rs"),
			contents: "pub fn broken(".to_string(),
		}],
	};

	let (_, warnings) = snapshot_public_symbols(Some(&snapshot), &[], DetectionLevel::Signature);

	assert_eq!(warnings.len(), 1);
	assert!(
		warnings
			.first()
			.unwrap_or_else(|| panic!("expected one parse warning"))
			.contains("failed to parse src/lib.rs")
	);
}

#[test]
fn manifest_helpers_cover_parse_failures_removed_entries_and_table_values() {
	let mut warnings = Vec::new();
	assert!(parse_manifest(Some("not = [valid"), Path::new("Cargo.toml"), &mut warnings).is_none());
	assert_eq!(warnings.len(), 1);

	let before = toml::from_str::<Value>(
		"[package]\nedition = \"2021\"\n\n[features]\ndefault = [\"cli\"]\n",
	)
	.unwrap_or_else(|error| panic!("parse before manifest: {error}"));
	let after = toml::from_str::<Value>("[package]\nedition = \"2024\"\n")
		.unwrap_or_else(|error| panic!("parse after manifest: {error}"));

	let before_metadata = extract_metadata_entries(&before);
	let after_metadata = extract_metadata_entries(&after);
	let changes = compare_manifest_entries(
		SemanticChangeCategory::Metadata,
		Path::new("Cargo.toml"),
		&before_metadata,
		&after_metadata,
	);

	assert!(changes.iter().any(|change| {
		change.item_path == "package.edition" && change.kind == SemanticChangeKind::Modified
	}));
	assert!(changes.iter().any(|change| {
		change.item_path == "feature.default" && change.kind == SemanticChangeKind::Removed
	}));
	let after_edition = after
		.get("package")
		.and_then(Value::as_table)
		.and_then(|package| package.get("edition"))
		.unwrap_or_else(|| panic!("expected package.edition"));
	assert_eq!(describe_manifest_value(after_edition), "2024");
	let before_default_feature = before
		.get("features")
		.and_then(Value::as_table)
		.and_then(|features| features.get("default"))
		.unwrap_or_else(|| panic!("expected features.default"));
	assert!(describe_manifest_value(before_default_feature).contains("cli"));
	let dependency_table = toml::from_str::<Value>("[dep]\nserde = \"1\"\n")
		.unwrap_or_else(|error| panic!("parse table manifest: {error}"));
	let dependency_value = dependency_table
		.get("dep")
		.unwrap_or_else(|| panic!("expected dep table"));
	assert!(describe_manifest_value(dependency_value).contains("serde=1"));
}

#[test]
fn module_prefix_diff_and_manifest_helpers_cover_remaining_branches() {
	assert!(module_prefix_for_file(Path::new("src")).is_empty());

	let before = BTreeMap::from([
		(
			("function".to_string(), "greet".to_string()),
			PublicSymbol {
				item_kind: "function".to_string(),
				item_path: "greet".to_string(),
				signature: "pub fn greet()".to_string(),
				file_path: PathBuf::from("src/lib.rs"),
			},
		),
		(
			("struct".to_string(), "Greeter".to_string()),
			PublicSymbol {
				item_kind: "struct".to_string(),
				item_path: "Greeter".to_string(),
				signature: "pub struct Greeter;".to_string(),
				file_path: PathBuf::from("src/lib.rs"),
			},
		),
	]);
	let after = BTreeMap::from([(
		("function".to_string(), "greet".to_string()),
		PublicSymbol {
			item_kind: "function".to_string(),
			item_path: "greet".to_string(),
			signature: "pub fn greet()".to_string(),
			file_path: PathBuf::from("src/lib.rs"),
		},
	)]);

	let changes = diff_public_symbols(&before, &after);

	assert_eq!(changes.len(), 1);
	let change = changes
		.first()
		.unwrap_or_else(|| panic!("expected one removed change"));
	assert_eq!(change.kind, SemanticChangeKind::Removed);
	assert!(change.summary.contains("removed"));
	assert_eq!(describe_manifest_value(&Value::Boolean(true)), "true");
}