use anyhow::Result;
use serde_json::{json, Value};
use std::path::PathBuf;
use tempfile::TempDir;
struct ConfigTestFixture {
#[allow(dead_code)] temp_dir: TempDir,
config_path: PathBuf,
}
impl ConfigTestFixture {
fn new() -> Result<Self> {
let temp_dir = tempfile::tempdir()?;
let config_path = temp_dir.path().join("pmat.toml");
let sample_config = r#"
[hooks]
enabled = true
auto_install = true
[hooks.quality_gates]
max_cyclomatic_complexity = 30
max_cognitive_complexity = 25
max_satd_comments = 5
min_test_coverage = 80.0
max_clippy_warnings = 100
[hooks.documentation]
required_files = [
"docs/execution/roadmap.md",
"CHANGELOG.md"
]
task_id_pattern = "PMAT-[0-9]{4}"
"#;
std::fs::write(&config_path, sample_config)?;
Ok(Self {
temp_dir,
config_path,
})
}
fn config_path(&self) -> &PathBuf {
&self.config_path
}
}
struct ConfigCommand {
#[allow(dead_code)] config_path: PathBuf,
}
impl ConfigCommand {
fn new(config_path: PathBuf) -> Self {
Self { config_path }
}
async fn show(&self, format: ConfigFormat) -> Result<String> {
let content = std::fs::read_to_string(&self.config_path)
.map_err(|e| anyhow::anyhow!("Failed to read config file: {}", e))?;
let toml_value: toml::Value =
toml::from_str(&content).map_err(|e| anyhow::anyhow!("Failed to parse TOML: {}", e))?;
match format {
ConfigFormat::Json => {
let json_value: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&toml_value)?)?;
Ok(serde_json::to_string_pretty(&json_value)?)
}
ConfigFormat::Toml => {
Ok(toml::to_string_pretty(&toml_value)?)
}
ConfigFormat::Env => {
self.toml_to_env_vars(&toml_value)
}
}
}
async fn get(&self, key: &str) -> Result<String> {
let content = std::fs::read_to_string(&self.config_path)
.map_err(|e| anyhow::anyhow!("Failed to read config file: {}", e))?;
let toml_value: toml::Value =
toml::from_str(&content).map_err(|e| anyhow::anyhow!("Failed to parse TOML: {}", e))?;
self.get_nested_value(&toml_value, key)
}
fn get_nested_value(&self, value: &toml::Value, key_path: &str) -> Result<String> {
let keys: Vec<&str> = key_path.split('.').collect();
let mut current = value;
for key in &keys {
match current {
toml::Value::Table(table) => {
current = table.get(*key).ok_or_else(|| {
anyhow::anyhow!("Key '{}' not found in path '{}'", key, key_path)
})?;
}
_ => {
return Err(anyhow::anyhow!(
"Cannot navigate further in path '{}' at key '{}'",
key_path,
key
));
}
}
}
match current {
toml::Value::String(s) => Ok(s.clone()),
toml::Value::Integer(i) => Ok(i.to_string()),
toml::Value::Float(f) => {
if f.fract() == 0.0 {
Ok(format!("{:.1}", f))
} else {
Ok(f.to_string())
}
}
toml::Value::Boolean(b) => Ok(b.to_string()),
toml::Value::Array(arr) => {
Ok(format!("Array with {} elements", arr.len()))
}
toml::Value::Table(_) => Err(anyhow::anyhow!(
"Path '{}' points to a table, not a value",
key_path
)),
toml::Value::Datetime(dt) => Ok(dt.to_string()),
}
}
async fn validate(&self) -> Result<ValidationResult> {
let content = match std::fs::read_to_string(&self.config_path) {
Ok(content) => content,
Err(_) => {
return Ok(ValidationResult {
is_valid: false,
errors: vec!["Configuration file not found".to_string()],
warnings: vec![],
});
}
};
let toml_value: toml::Value = match toml::from_str(&content) {
Ok(value) => value,
Err(e) => {
return Ok(ValidationResult {
is_valid: false,
errors: vec![format!("Invalid TOML syntax: {}", e)],
warnings: vec![],
});
}
};
let mut errors = Vec::new();
let mut warnings = Vec::new();
self.validate_config_structure(&toml_value, &mut errors, &mut warnings)?;
Ok(ValidationResult {
is_valid: errors.is_empty(),
errors,
warnings,
})
}
fn validate_config_structure(
&self,
toml_value: &toml::Value,
errors: &mut Vec<String>,
_warnings: &mut Vec<String>,
) -> Result<()> {
if let toml::Value::Table(root_table) = toml_value {
if let Some(hooks_table) = root_table.get("hooks").and_then(|v| v.as_table()) {
if hooks_table.get("quality_gates").is_none() {
errors.push("Missing required section: hooks.quality_gates".to_string());
}
if let Some(doc_table) = hooks_table.get("documentation").and_then(|v| v.as_table())
{
if let Some(pattern) = doc_table.get("task_id_pattern").and_then(|v| v.as_str())
{
if pattern.contains("[") && !pattern.contains("]") {
errors.push("Invalid task_id_pattern: unclosed bracket".to_string());
}
}
}
} else {
errors.push("Missing required section: hooks".to_string());
}
} else {
errors.push("Configuration root must be a table".to_string());
}
Ok(())
}
fn toml_to_env_vars(&self, toml_value: &toml::Value) -> Result<String> {
let mut env_vars = Vec::new();
if let toml::Value::Table(root_table) = toml_value {
if let Some(hooks_table) = root_table.get("hooks").and_then(|v| v.as_table()) {
if let Some(enabled) = hooks_table.get("enabled").and_then(|v| v.as_bool()) {
env_vars.push(format!("PMAT_HOOKS_ENABLED={}", enabled));
}
if let Some(auto_install) =
hooks_table.get("auto_install").and_then(|v| v.as_bool())
{
env_vars.push(format!("PMAT_HOOKS_AUTO_INSTALL={}", auto_install));
}
if let Some(quality_gates) =
hooks_table.get("quality_gates").and_then(|v| v.as_table())
{
if let Some(max_cyclomatic) = quality_gates
.get("max_cyclomatic_complexity")
.and_then(|v| v.as_integer())
{
env_vars.push(format!("PMAT_MAX_CYCLOMATIC_COMPLEXITY={}", max_cyclomatic));
}
if let Some(max_cognitive) = quality_gates
.get("max_cognitive_complexity")
.and_then(|v| v.as_integer())
{
env_vars.push(format!("PMAT_MAX_COGNITIVE_COMPLEXITY={}", max_cognitive));
}
if let Some(max_satd) = quality_gates
.get("max_satd_comments")
.and_then(|v| v.as_integer())
{
env_vars.push(format!("PMAT_MAX_SATD_COMMENTS={}", max_satd));
}
if let Some(min_coverage) = quality_gates
.get("min_test_coverage")
.and_then(|v| v.as_float())
{
env_vars.push(format!("PMAT_MIN_TEST_COVERAGE={}", min_coverage as i64));
}
if let Some(max_clippy) = quality_gates
.get("max_clippy_warnings")
.and_then(|v| v.as_integer())
{
env_vars.push(format!("PMAT_MAX_CLIPPY_WARNINGS={}", max_clippy));
}
}
}
}
Ok(env_vars.join("\n"))
}
}
#[derive(Debug, Clone)]
enum ConfigFormat {
Json,
Toml,
Env,
}
#[derive(Debug, PartialEq)]
struct ValidationResult {
is_valid: bool,
errors: Vec<String>,
warnings: Vec<String>,
}
#[tokio::test]
async fn test_config_show_json_format() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let result = config_cmd.show(ConfigFormat::Json).await?;
let parsed: Value = serde_json::from_str(&result)?;
assert_eq!(parsed["hooks"]["enabled"], true);
assert_eq!(parsed["hooks"]["auto_install"], true);
assert_eq!(
parsed["hooks"]["quality_gates"]["max_cyclomatic_complexity"],
30
);
assert_eq!(
parsed["hooks"]["quality_gates"]["max_cognitive_complexity"],
25
);
assert_eq!(parsed["hooks"]["quality_gates"]["max_satd_comments"], 5);
assert_eq!(parsed["hooks"]["quality_gates"]["min_test_coverage"], 80.0);
let required_files = parsed["hooks"]["documentation"]["required_files"]
.as_array()
.unwrap();
assert!(required_files.contains(&json!("docs/execution/roadmap.md")));
assert!(required_files.contains(&json!("CHANGELOG.md")));
assert_eq!(
parsed["hooks"]["documentation"]["task_id_pattern"],
"PMAT-[0-9]{4}"
);
Ok(())
}
#[tokio::test]
async fn test_config_show_toml_format() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let result = config_cmd.show(ConfigFormat::Toml).await?;
assert!(result.contains("[hooks]"));
assert!(result.contains("enabled = true"));
assert!(result.contains("auto_install = true"));
assert!(result.contains("[hooks.quality_gates]"));
assert!(result.contains("max_cyclomatic_complexity = 30"));
assert!(result.contains("max_cognitive_complexity = 25"));
Ok(())
}
#[tokio::test]
async fn test_config_show_env_format() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let result = config_cmd.show(ConfigFormat::Env).await?;
assert!(result.contains("PMAT_HOOKS_ENABLED=true"));
assert!(result.contains("PMAT_HOOKS_AUTO_INSTALL=true"));
assert!(result.contains("PMAT_MAX_CYCLOMATIC_COMPLEXITY=30"));
assert!(result.contains("PMAT_MAX_COGNITIVE_COMPLEXITY=25"));
assert!(result.contains("PMAT_MAX_SATD_COMMENTS=5"));
assert!(result.contains("PMAT_MIN_TEST_COVERAGE=80"));
Ok(())
}
#[tokio::test]
async fn test_config_get_specific_values() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let max_complexity = config_cmd
.get("hooks.quality_gates.max_cyclomatic_complexity")
.await?;
assert_eq!(max_complexity, "30");
let auto_install = config_cmd.get("hooks.auto_install").await?;
assert_eq!(auto_install, "true");
let coverage = config_cmd
.get("hooks.quality_gates.min_test_coverage")
.await?;
assert_eq!(coverage, "80.0");
let pattern = config_cmd
.get("hooks.documentation.task_id_pattern")
.await?;
assert_eq!(pattern, "PMAT-[0-9]{4}");
Ok(())
}
#[tokio::test]
async fn test_config_get_nonexistent_key() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let result = config_cmd.get("nonexistent.key.path").await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_config_validate_valid_config() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let result = config_cmd.validate().await?;
assert!(result.is_valid);
assert!(result.errors.is_empty());
Ok(())
}
#[tokio::test]
async fn test_config_validate_invalid_config() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let invalid_config_path = temp_dir.path().join("invalid.toml");
let invalid_config = r#"
[hooks]
# Missing required quality_gates section
enabled = true
[hooks.documentation]
# Invalid pattern
task_id_pattern = "[invalid regex"
"#;
std::fs::write(&invalid_config_path, invalid_config)?;
let config_cmd = ConfigCommand::new(invalid_config_path);
let result = config_cmd.validate().await?;
assert!(!result.is_valid);
assert!(!result.errors.is_empty());
Ok(())
}
#[tokio::test]
async fn test_config_missing_file() -> Result<()> {
let nonexistent_path = PathBuf::from("/nonexistent/config.toml");
let config_cmd = ConfigCommand::new(nonexistent_path);
let result = config_cmd.show(ConfigFormat::Json).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_config_performance_requirements() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let start = std::time::Instant::now();
let _result = config_cmd.show(ConfigFormat::Json).await?;
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 100,
"Config loading took {}ms (should be <100ms)",
elapsed.as_millis()
);
Ok(())
}
#[tokio::test]
async fn test_config_format_roundtrip() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let original_toml = config_cmd.show(ConfigFormat::Toml).await?;
let json_version = config_cmd.show(ConfigFormat::Json).await?;
let original_parsed: toml::Value = toml::from_str(&original_toml)?;
let json_parsed: serde_json::Value = serde_json::from_str(&json_version)?;
assert_eq!(
original_parsed["hooks"]["quality_gates"]["max_cyclomatic_complexity"]
.as_integer()
.unwrap(),
json_parsed["hooks"]["quality_gates"]["max_cyclomatic_complexity"]
.as_i64()
.unwrap()
);
assert_eq!(
original_parsed["hooks"]["enabled"].as_bool().unwrap(),
json_parsed["hooks"]["enabled"].as_bool().unwrap()
);
Ok(())
}
#[tokio::test]
async fn test_extract_config_error_handler_max_complexity() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let config_path = temp_dir.path().join("invalid.toml");
let invalid_config = r#"
[quality]
max_complexity = 0 # Invalid: must be > 0
"#;
std::fs::write(&config_path, invalid_config)?;
let errors = ["max_complexity must be > 0".to_string()];
let result = extract_config_error_handler(&errors[0]);
assert!(result.is_some());
let fix_info = result.unwrap();
assert_eq!(fix_info.field_name, "quality.max_complexity");
assert_eq!(fix_info.new_value, "20");
assert_eq!(fix_info.description, "Set max_complexity to 20");
Ok(())
}
#[tokio::test]
async fn test_extract_config_error_handler_min_coverage() -> Result<()> {
let errors = ["min_coverage must be between 0 and 100".to_string()];
let result = extract_config_error_handler(&errors[0]);
assert!(result.is_some());
let fix_info = result.unwrap();
assert_eq!(fix_info.field_name, "quality.min_coverage");
assert!(fix_info.description.contains("Clamped min_coverage"));
Ok(())
}
#[tokio::test]
async fn test_extract_config_error_handler_unknown_error() -> Result<()> {
let unknown_error = "some unknown config error";
let result = extract_config_error_handler(unknown_error);
assert!(result.is_none());
Ok(())
}
#[tokio::test]
async fn test_apply_config_fixes_complexity_10() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let config_path = temp_dir.path().join("test.toml");
let config_content = r#"
[quality]
max_complexity = 0
min_coverage = 150.0
"#;
std::fs::write(&config_path, config_content)?;
let errors = vec![
"max_complexity must be > 0".to_string(),
"min_coverage must be between 0 and 100".to_string(),
];
let fixed_issues = apply_config_fixes(&errors).await?;
assert_eq!(fixed_issues.len(), 2);
assert!(fixed_issues.contains(&"Set max_complexity to 20".to_string()));
assert!(fixed_issues
.iter()
.any(|fix| fix.contains("Clamped min_coverage")));
Ok(())
}
#[tokio::test]
async fn test_save_config_changes_complexity_10() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let config_path = temp_dir.path().join("test.toml");
let original_config = r#"
[quality]
max_complexity = 0
"#;
std::fs::write(&config_path, original_config)?;
let fixed_issues = vec!["Set max_complexity to 20".to_string()];
let result = save_config_changes(&config_path, &fixed_issues).await;
assert!(result.is_ok());
let updated_content = std::fs::read_to_string(&config_path)?;
assert!(
updated_content.contains("max_complexity = 20")
|| updated_content.contains("fixed configuration")
);
Ok(())
}
#[derive(Debug, PartialEq)]
struct ConfigFixInfo {
field_name: String,
new_value: String,
description: String,
}
fn extract_config_error_handler(error_msg: &str) -> Option<ConfigFixInfo> {
if error_msg.contains("max_complexity must be > 0") {
return Some(ConfigFixInfo {
field_name: "quality.max_complexity".to_string(),
new_value: "20".to_string(),
description: "Set max_complexity to 20".to_string(),
});
}
if error_msg.contains("min_coverage must be between 0 and 100") {
return Some(ConfigFixInfo {
field_name: "quality.min_coverage".to_string(),
new_value: "clamp(0.0, 100.0)".to_string(),
description: "Clamped min_coverage to valid range".to_string(),
});
}
None
}
async fn apply_config_fixes(errors: &[String]) -> Result<Vec<String>> {
let mut fixed_issues = Vec::new();
for error in errors {
if let Some(fix_info) = extract_config_error_handler(error) {
fixed_issues.push(fix_info.description);
}
}
Ok(fixed_issues)
}
async fn save_config_changes(config_path: &std::path::Path, fixed_issues: &[String]) -> Result<()> {
if fixed_issues.is_empty() {
return Ok(());
}
let mut content = std::fs::read_to_string(config_path)?;
if fixed_issues
.iter()
.any(|fix| fix.contains("max_complexity"))
{
content = content.replace("max_complexity = 0", "max_complexity = 20");
}
std::fs::write(config_path, content)?;
Ok(())
}
#[tokio::test]
async fn test_config_integration_with_quality_gates() -> Result<()> {
let fixture = ConfigTestFixture::new()?;
let config_cmd = ConfigCommand::new(fixture.config_path().clone());
let max_complexity = config_cmd
.get("hooks.quality_gates.max_cyclomatic_complexity")
.await?;
let max_cognitive = config_cmd
.get("hooks.quality_gates.max_cognitive_complexity")
.await?;
let complexity_threshold: u32 = max_complexity.parse()?;
let cognitive_threshold: u32 = max_cognitive.parse()?;
assert_eq!(complexity_threshold, 30);
assert_eq!(cognitive_threshold, 25);
assert!(
complexity_threshold >= 20,
"Cyclomatic threshold should be enterprise-standard (≥20)"
);
assert!(
cognitive_threshold >= 20,
"Cognitive threshold should be enterprise-standard (≥20)"
);
Ok(())
}