use std::path::Path;
use switchy_fs::TempDir;
use clippier::OutputType;
use clippier::feature_validator::{FeatureValidator, ValidatorConfig};
fn create_complex_workspace() -> TempDir {
let temp_dir = switchy_fs::tempdir().unwrap();
let root_path = temp_dir.path();
let workspace_cargo = r#"[workspace]
members = [
"core",
"web_api",
"auth",
"database",
"utils",
"external_lib",
]
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
anyhow = "1.0"
uuid = "1.0"
reqwest = "0.11"
"#;
switchy_fs::sync::write(root_path.join("Cargo.toml"), workspace_cargo).unwrap();
create_package(
root_path,
"core",
r#"[package]
name = "core"
version = "0.1.0"
[dependencies]
auth = { path = "../auth" }
database = { path = "../database", optional = true }
utils = { path = "../utils" }
serde = { workspace = true }
anyhow = { workspace = true }
[features]
default = ["auth/default"]
fail-on-warnings = [
"auth/fail-on-warnings",
"database?/fail-on-warnings",
"utils/fail-on-warnings",
]
full = ["database", "auth/full", "database?/full", "utils/full"]
json = ["serde/json", "utils/json"]
"#,
);
create_package(
root_path,
"web_api",
r#"[package]
name = "web_api"
version = "0.1.0"
[dependencies]
core = { path = "../core" }
auth = { path = "../auth" }
tokio = { workspace = true }
reqwest = { workspace = true, optional = true }
[features]
fail-on-warnings = [
"core/fail-on-warnings",
"auth/fail-on-warnings",
"reqwest?/fail-on-warnings",
]
client = ["dep:reqwest", "core/json"]
full = ["client", "core/full", "auth/full"]
"#,
);
create_package(
root_path,
"auth",
r#"[package]
name = "auth"
version = "0.1.0"
[dependencies]
utils = { path = "../utils" }
uuid = { workspace = true }
external_lib = { path = "../external_lib", optional = true }
[features]
default = ["basic"]
basic = ["utils/basic"]
fail-on-warnings = [
"utils/fail-on-warnings",
"external_lib?/fail-on-warnings",
]
full = ["basic", "external_lib", "utils/full"]
jwt = ["external_lib?/jwt"]
"#,
);
create_package(
root_path,
"database",
r#"[package]
name = "database"
version = "0.1.0"
[dependencies]
utils = { path = "../utils" }
anyhow = { workspace = true }
[features]
fail-on-warnings = ["utils/fail-on-warnings"]
full = ["utils/full"]
migrations = ["utils/fs"]
"#,
);
create_package(
root_path,
"utils",
r#"[package]
name = "utils"
version = "0.1.0"
[dependencies]
serde = { workspace = true, optional = true }
anyhow = { workspace = true }
[features]
default = []
fail-on-warnings = []
basic = []
full = ["json", "fs", "basic"]
json = ["dep:serde"]
fs = []
"#,
);
create_package(
root_path,
"external_lib",
r#"[package]
name = "external_lib"
version = "0.1.0"
[dependencies]
anyhow = { workspace = true }
[features]
fail-on-warnings = []
jwt = []
crypto = []
"#,
);
temp_dir
}
fn create_error_workspace() -> TempDir {
let temp_dir = switchy_fs::tempdir().unwrap();
let root_path = temp_dir.path();
let workspace_cargo = r#"[workspace]
members = ["main", "lib_a", "lib_b"]
[workspace.dependencies]
serde = "1.0"
"#;
switchy_fs::sync::write(root_path.join("Cargo.toml"), workspace_cargo).unwrap();
create_package(
root_path,
"main",
r#"[package]
name = "main"
version = "0.1.0"
[dependencies]
lib_a = { path = "../lib_a" }
lib_b = { path = "../lib_b", optional = true }
serde = { workspace = true }
[features]
# Missing propagation to lib_a
fail-on-warnings = ["lib_b?/fail-on-warnings"]
# Incorrect propagation - lib_a doesn't have this feature
test-utils = ["lib_a/nonexistent-feature"]
# Missing optional dependency marker
missing-optional = ["lib_b/test-utils"]
"#,
);
create_package(
root_path,
"lib_a",
r#"[package]
name = "lib_a"
version = "0.1.0"
[dependencies]
serde = { workspace = true }
[features]
fail-on-warnings = []
test-utils = []
"#,
);
create_package(
root_path,
"lib_b",
r#"[package]
name = "lib_b"
version = "0.1.0"
[features]
fail-on-warnings = []
test-utils = []
"#,
);
temp_dir
}
fn create_single_package_project() -> TempDir {
let temp_dir = switchy_fs::tempdir().unwrap();
let root_path = temp_dir.path();
let cargo_toml = r#"[package]
name = "single_package"
version = "0.1.0"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
[features]
default = []
fail-on-warnings = []
json = []
"#;
switchy_fs::sync::write(root_path.join("Cargo.toml"), cargo_toml).unwrap();
temp_dir
}
fn create_package(workspace_root: &Path, name: &str, cargo_content: &str) {
let pkg_dir = workspace_root.join(name);
switchy_fs::sync::create_dir(&pkg_dir).unwrap();
switchy_fs::sync::write(pkg_dir.join("Cargo.toml"), cargo_content).unwrap();
let src_dir = pkg_dir.join("src");
switchy_fs::sync::create_dir(&src_dir).unwrap();
switchy_fs::sync::write(src_dir.join("lib.rs"), "// Auto-generated for testing\n").unwrap();
}
#[switchy_async::test]
async fn test_complex_workspace_validation_success() {
let workspace = create_complex_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: Some(vec!["fail-on-warnings".to_string()]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert_eq!(
result.errors.len(),
0,
"Expected no validation errors, got: {:#?}",
result.errors
);
assert!(
result.valid_packages > 0,
"Should have validated at least one package"
);
assert_eq!(result.total_packages, result.valid_packages);
}
#[switchy_async::test]
async fn test_complex_workspace_all_features() {
let workspace = create_complex_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: None, skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert!(
result.total_packages > 0,
"Should validate at least some packages"
);
if !result.errors.is_empty() {
eprintln!(
"Note: Found {} validation errors (likely from external dependencies): {:#?}",
result.errors.len(),
result.errors
);
}
}
#[switchy_async::test]
async fn test_workspace_with_errors() {
let workspace = create_error_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: None, skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert!(!result.errors.is_empty(), "Expected validation errors");
let main_package_error = result
.errors
.iter()
.find(|e| e.package == "main")
.expect("Should find errors in main package");
assert!(
!main_package_error.errors.is_empty(),
"Should have feature errors"
);
let fail_on_warnings_error = main_package_error
.errors
.iter()
.find(|e| e.feature == "fail-on-warnings")
.expect("Should have fail-on-warnings error");
assert!(
!fail_on_warnings_error.missing_propagations.is_empty(),
"Should have missing propagation to lib_a"
);
let test_utils_error = main_package_error
.errors
.iter()
.find(|e| e.feature == "test-utils");
if let Some(error) = test_utils_error {
assert!(
!error.incorrect_propagations.is_empty(),
"Should have incorrect propagation for nonexistent feature"
);
}
}
#[switchy_async::test]
async fn test_single_package_project() {
let project = create_single_package_project();
let root_path = project.path().to_path_buf();
let config = ValidatorConfig {
features: Some(vec!["fail-on-warnings".to_string()]),
skip_features: None,
workspace_only: false,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert_eq!(result.errors.len(), 0);
assert_eq!(result.valid_packages, 1);
}
#[switchy_async::test]
async fn test_workspace_only_vs_all_packages() {
let workspace = create_complex_workspace();
let root_path = workspace.path().to_path_buf();
let config_workspace_only = ValidatorConfig {
features: Some(vec!["fail-on-warnings".to_string()]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator_workspace =
FeatureValidator::new(Some(root_path.clone()), config_workspace_only).unwrap();
let result_workspace = validator_workspace.validate().unwrap();
let config_all = ValidatorConfig {
features: Some(vec!["fail-on-warnings".to_string()]),
skip_features: None,
workspace_only: false,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator_all = FeatureValidator::new(Some(root_path), config_all).unwrap();
let result_all = validator_all.validate().unwrap();
assert_eq!(result_workspace.errors.len(), 0);
assert!(result_all.total_packages >= result_workspace.total_packages);
}
#[switchy_async::test]
async fn test_json_output_format() {
let workspace = create_error_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: Some(vec![
"fail-on-warnings".to_string(),
"test-utils".to_string(),
]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Json,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
let json_output = serde_json::to_string_pretty(&result).unwrap();
assert!(json_output.contains("total_packages"));
assert!(json_output.contains("valid_packages"));
assert!(json_output.contains("errors"));
if !result.errors.is_empty() {
assert!(
json_output.contains("missing_propagations")
|| json_output.contains("incorrect_propagations")
);
}
}
#[switchy_async::test]
async fn test_specific_features_validation() {
let workspace = create_complex_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: Some(vec!["full".to_string(), "json".to_string()]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert_eq!(result.errors.len(), 0);
assert!(result.total_packages > 0);
}
#[switchy_async::test]
async fn test_validator_with_nonexistent_feature() {
let workspace = create_complex_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: Some(vec!["nonexistent-feature".to_string()]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert_eq!(result.errors.len(), 0);
assert_eq!(result.valid_packages, result.total_packages);
}
#[switchy_async::test]
async fn test_optional_dependency_handling() {
let workspace = create_complex_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: Some(vec!["full".to_string()]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert_eq!(
result.errors.len(),
0,
"Optional dependency handling failed: {:#?}",
result.errors
);
}
#[switchy_async::test]
async fn test_workspace_root_discovery_from_subdirectory() {
let workspace = create_complex_workspace();
let core_path = workspace.path().join("core");
let config = ValidatorConfig {
features: Some(vec!["fail-on-warnings".to_string()]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(core_path), config).unwrap();
let result = validator.validate().unwrap();
assert!(
result.total_packages > 1,
"Should find multiple packages from subdirectory"
);
assert_eq!(result.errors.len(), 0);
}
#[switchy_async::test]
async fn test_validation_summary_with_errors() {
let workspace = create_error_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: None,
skip_features: None,
workspace_only: true,
output_format: OutputType::Raw,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert!(
!result.errors.is_empty(),
"Should have errors to display in summary"
);
assert!(result.total_packages > 0);
assert!(result.valid_packages < result.total_packages);
assert_eq!(
result.total_packages,
result.valid_packages + result.errors.len()
);
}
#[switchy_async::test]
async fn test_validation_summary_with_no_errors() {
let workspace = create_complex_workspace();
let root_path = workspace.path().to_path_buf();
let config = ValidatorConfig {
features: Some(vec!["fail-on-warnings".to_string()]),
skip_features: None,
workspace_only: true,
output_format: OutputType::Json,
strict_optional_propagation: false,
..ValidatorConfig::test_default()
};
let validator = FeatureValidator::new(Some(root_path), config).unwrap();
let result = validator.validate().unwrap();
assert_eq!(result.errors.len(), 0);
assert!(result.valid_packages > 0);
assert_eq!(result.total_packages, result.valid_packages);
}
#[switchy_async::test]
async fn test_validation_result_json_serialization() {
use clippier::feature_validator::{
FeatureError, IncorrectPropagation, MissingPropagation, PackageValidationError,
ValidationResult,
};
let result = ValidationResult {
total_packages: 3,
valid_packages: 2,
errors: vec![PackageValidationError {
package: "test_pkg".to_string(),
errors: vec![FeatureError {
feature: "fail-on-warnings".to_string(),
missing_propagations: vec![MissingPropagation {
dependency: "dep1".to_string(),
expected: "dep1/fail-on-warnings".to_string(),
reason: "Dependency has feature but not propagated".to_string(),
}],
incorrect_propagations: vec![IncorrectPropagation {
entry: "nonexistent/feature".to_string(),
reason: "Dependency doesn't have this feature".to_string(),
}],
}],
}],
warnings: vec![],
overridden_errors: vec![],
override_summary: None,
parent_results: vec![],
};
let json = serde_json::to_string_pretty(&result).unwrap();
assert!(json.contains("test_pkg"));
assert!(json.contains("fail-on-warnings"));
assert!(json.contains("dep1"));
assert!(json.contains("missing_propagations"));
assert!(json.contains("incorrect_propagations"));
let _parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
}
#[switchy_async::test]
async fn test_validation_summary_pluralization() {
use clippier::feature_validator::{PackageValidationError, ValidationResult};
let result_singular = ValidationResult {
total_packages: 3,
valid_packages: 2,
errors: vec![PackageValidationError {
package: "test".to_string(),
errors: vec![],
}],
warnings: vec![],
overridden_errors: vec![],
override_summary: None,
parent_results: vec![],
};
assert_eq!(result_singular.errors.len(), 1);
assert_eq!(result_singular.valid_packages, 2);
let result_plural = ValidationResult {
total_packages: 5,
valid_packages: 3,
errors: vec![
PackageValidationError {
package: "test1".to_string(),
errors: vec![],
},
PackageValidationError {
package: "test2".to_string(),
errors: vec![],
},
],
warnings: vec![],
overridden_errors: vec![],
override_summary: None,
parent_results: vec![],
};
assert_eq!(result_plural.errors.len(), 2);
assert_eq!(result_plural.valid_packages, 3);
assert_eq!(
result_plural.total_packages,
result_plural.valid_packages + result_plural.errors.len()
);
}