#![cfg_attr(coverage_nightly, coverage(off))]
use super::types::{Mutant, MutationResult};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationState {
pub project_path: PathBuf,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub completed_mutants: Vec<MutationResult>,
pub pending_mutants: Vec<Mutant>,
pub config: MutationStateConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationStateConfig {
pub timeout_secs: u64,
pub worker_count: Option<usize>,
pub parallel: bool,
}
impl MutationState {
pub fn new(
project_path: &Path,
mutants: Vec<Mutant>,
timeout_secs: u64,
parallel: bool,
worker_count: Option<usize>,
) -> Self {
Self {
project_path: project_path.to_path_buf(),
timestamp: chrono::Utc::now(),
completed_mutants: Vec::new(),
pending_mutants: mutants,
config: MutationStateConfig {
timeout_secs,
worker_count,
parallel,
},
}
}
pub fn is_complete(&self) -> bool {
self.pending_mutants.is_empty()
}
pub fn total_mutants(&self) -> usize {
self.completed_mutants.len() + self.pending_mutants.len()
}
pub fn completed_count(&self) -> usize {
self.completed_mutants.len()
}
pub fn completion_percentage(&self) -> f64 {
if self.total_mutants() == 0 {
return 100.0;
}
(self.completed_count() as f64 / self.total_mutants() as f64) * 100.0
}
pub fn add_result(&mut self, result: MutationResult) {
self.pending_mutants.retain(|m| m.id != result.mutant.id);
self.completed_mutants.push(result);
self.timestamp = chrono::Utc::now();
}
pub async fn save(&self, path: &Path) -> Result<()> {
let json =
serde_json::to_string_pretty(self).context("Failed to serialize mutation state")?;
fs::write(path, json)
.await
.with_context(|| format!("Failed to write mutation state to {}", path.display()))?;
Ok(())
}
pub async fn load(path: &Path) -> Result<Self> {
let json = fs::read_to_string(path)
.await
.with_context(|| format!("Failed to read mutation state from {}", path.display()))?;
let state = serde_json::from_str(&json).context("Failed to parse mutation state")?;
Ok(state)
}
pub fn default_state_path(project_path: &Path) -> PathBuf {
project_path.join(".pmat").join("mutation_state.json")
}
pub async fn save_with_backup(&self, path: &Path) -> Result<()> {
if path.exists() {
let backup_path = path.with_extension("json.bak");
fs::copy(path, &backup_path)
.await
.with_context(|| "Failed to create backup of state file".to_string())?;
}
self.save(path).await
}
}
#[cfg(test)]
#[cfg(any())] mod tests {
use super::*;
use crate::services::mutation::types::MutantStatus;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_mutation_state_creation() {
let project_path = PathBuf::from("/test/project");
let mutants = vec![
Mutant {
id: 1,
original_file: PathBuf::from("src/main.rs"),
mutated_source: "mutated code 1".to_string(),
original_source: "original code 1".to_string(),
line_number: 10,
mutation_description: "test mutation 1".to_string(),
mutation_operator: "test".to_string(),
mutation_origin: MutationOrigin::Ast,
},
Mutant {
id: 2,
original_file: PathBuf::from("src/lib.rs"),
mutated_source: "mutated code 2".to_string(),
original_source: "original code 2".to_string(),
line_number: 20,
mutation_description: "test mutation 2".to_string(),
mutation_operator: "test".to_string(),
mutation_origin: MutationOrigin::Ast,
},
];
let state = MutationState::new(&project_path, mutants, 60, true, Some(4));
assert_eq!(state.project_path, project_path);
assert_eq!(state.completed_mutants.len(), 0);
assert_eq!(state.pending_mutants.len(), 2);
assert!(!state.is_complete());
assert_eq!(state.total_mutants(), 2);
assert_eq!(state.completed_count(), 0);
assert_eq!(state.completion_percentage(), 0.0);
assert_eq!(state.config.timeout_secs, 60);
assert_eq!(state.config.worker_count, Some(4));
assert!(state.config.parallel);
}
#[tokio::test]
async fn test_mutation_state_add_result() {
let project_path = PathBuf::from("/test/project");
let mutants = vec![
Mutant {
id: 1,
original_file: PathBuf::from("src/main.rs"),
mutated_source: "mutated code 1".to_string(),
original_source: "original code 1".to_string(),
line_number: 10,
mutation_description: "test mutation 1".to_string(),
mutation_operator: "test".to_string(),
mutation_origin: MutationOrigin::Ast,
},
Mutant {
id: 2,
original_file: PathBuf::from("src/lib.rs"),
mutated_source: "mutated code 2".to_string(),
original_source: "original code 2".to_string(),
line_number: 20,
mutation_description: "test mutation 2".to_string(),
mutation_operator: "test".to_string(),
mutation_origin: MutationOrigin::Ast,
},
];
let mut state = MutationState::new(&project_path, mutants, 60, true, Some(4));
let result1 = MutationResult {
mutant: state.pending_mutants[0].clone(),
status: MutantStatus::Killed,
test_failures: vec!["test_failure".to_string()],
execution_time_ms: 100,
error_message: None,
};
state.add_result(result1);
assert_eq!(state.completed_mutants.len(), 1);
assert_eq!(state.pending_mutants.len(), 1);
assert!(!state.is_complete());
assert_eq!(state.total_mutants(), 2);
assert_eq!(state.completed_count(), 1);
assert_eq!(state.completion_percentage(), 50.0);
let result2 = MutationResult {
mutant: state.pending_mutants[0].clone(),
status: MutantStatus::Survived,
test_failures: vec![],
execution_time_ms: 200,
error_message: None,
};
state.add_result(result2);
assert_eq!(state.completed_mutants.len(), 2);
assert_eq!(state.pending_mutants.len(), 0);
assert!(state.is_complete());
assert_eq!(state.total_mutants(), 2);
assert_eq!(state.completed_count(), 2);
assert_eq!(state.completion_percentage(), 100.0);
}
#[tokio::test]
async fn test_mutation_state_save_load() -> Result<()> {
let temp_dir = TempDir::new()?;
let state_path = temp_dir.path().join("mutation_state.json");
let project_path = PathBuf::from("/test/project");
let mutants = vec![Mutant {
id: 1,
original_file: PathBuf::from("src/main.rs"),
mutated_source: "mutated code".to_string(),
original_source: "original code".to_string(),
line_number: 10,
mutation_description: "test mutation".to_string(),
mutation_operator: "test".to_string(),
mutation_origin: MutationOrigin::Ast,
}];
let state = MutationState::new(&project_path, mutants, 60, true, Some(4));
state.save(&state_path).await?;
assert!(state_path.exists());
let loaded_state = MutationState::load(&state_path).await?;
assert_eq!(loaded_state.project_path, project_path);
assert_eq!(loaded_state.completed_mutants.len(), 0);
assert_eq!(loaded_state.pending_mutants.len(), 1);
assert_eq!(loaded_state.pending_mutants[0].id, 1);
assert_eq!(loaded_state.config.timeout_secs, 60);
Ok(())
}
#[tokio::test]
async fn test_mutation_state_save_with_backup() -> Result<()> {
let temp_dir = TempDir::new()?;
let state_path = temp_dir.path().join("mutation_state.json");
let project_path = PathBuf::from("/test/project");
let initial_mutants = vec![Mutant {
id: 1,
original_file: PathBuf::from("src/main.rs"),
mutated_source: "initial mutated code".to_string(),
original_source: "initial original code".to_string(),
line_number: 10,
mutation_description: "initial test mutation".to_string(),
mutation_operator: "test".to_string(),
mutation_origin: MutationOrigin::Ast,
}];
let initial_state = MutationState::new(&project_path, initial_mutants, 60, true, Some(4));
initial_state.save(&state_path).await?;
let updated_mutants = vec![Mutant {
id: 2,
original_file: PathBuf::from("src/lib.rs"),
mutated_source: "updated mutated code".to_string(),
original_source: "updated original code".to_string(),
line_number: 20,
mutation_description: "updated test mutation".to_string(),
mutation_operator: "test".to_string(),
mutation_origin: MutationOrigin::Ast,
}];
let updated_state = MutationState::new(&project_path, updated_mutants, 120, false, None);
updated_state.save_with_backup(&state_path).await?;
let backup_path = state_path.with_extension("json.bak");
assert!(backup_path.exists());
let backup_json = fs::read_to_string(backup_path)?;
let backup_state: MutationState = serde_json::from_str(&backup_json)?;
assert_eq!(backup_state.pending_mutants.len(), 1);
assert_eq!(backup_state.pending_mutants[0].id, 1);
assert_eq!(backup_state.config.timeout_secs, 60);
let updated_json = fs::read_to_string(&state_path)?;
let loaded_updated_state: MutationState = serde_json::from_str(&updated_json)?;
assert_eq!(loaded_updated_state.pending_mutants.len(), 1);
assert_eq!(loaded_updated_state.pending_mutants[0].id, 2);
assert_eq!(loaded_updated_state.config.timeout_secs, 120);
Ok(())
}
}