pmat 3.15.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
#![cfg_attr(coverage_nightly, coverage(off))]
//! Mutation testing state persistence
//!
//! Provides state management for resumable mutation testing,
//! allowing interrupted tests to be resumed from where they left off.

use super::types::{Mutant, MutationResult};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;

/// State of a mutation testing run
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationState {
    /// Project path being tested
    pub project_path: PathBuf,

    /// Timestamp when the state was created/updated
    pub timestamp: chrono::DateTime<chrono::Utc>,

    /// Mutants that have been completed
    pub completed_mutants: Vec<MutationResult>,

    /// Mutants that are pending execution
    pub pending_mutants: Vec<Mutant>,

    /// Configuration used for the test run
    pub config: MutationStateConfig,
}

/// Configuration for mutation testing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationStateConfig {
    /// Timeout for test execution in seconds
    pub timeout_secs: u64,

    /// Number of workers for parallel execution
    pub worker_count: Option<usize>,

    /// Whether to use parallel execution
    pub parallel: bool,
}

impl MutationState {
    /// Create a new mutation state for a set of mutants
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
    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,
            },
        }
    }

    /// Check if all mutants have been completed
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn is_complete(&self) -> bool {
        self.pending_mutants.is_empty()
    }

    /// Get the total number of mutants
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn total_mutants(&self) -> usize {
        self.completed_mutants.len() + self.pending_mutants.len()
    }

    /// Get the number of completed mutants
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn completed_count(&self) -> usize {
        self.completed_mutants.len()
    }

    /// Get completion percentage
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    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
    }

    /// Add a completed mutant result
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn add_result(&mut self, result: MutationResult) {
        // Remove the mutant from pending
        self.pending_mutants.retain(|m| m.id != result.mutant.id);

        // Add to completed
        self.completed_mutants.push(result);

        // Update timestamp
        self.timestamp = chrono::Utc::now();
    }

    /// Save mutation state to disk
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
    pub async fn save(&self, path: &Path) -> Result<()> {
        // Serialize to JSON with pretty formatting
        let json =
            serde_json::to_string_pretty(self).context("Failed to serialize mutation state")?;

        // Write to file
        fs::write(path, json)
            .await
            .with_context(|| format!("Failed to write mutation state to {}", path.display()))?;

        Ok(())
    }

    /// Load mutation state from disk
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
    pub async fn load(path: &Path) -> Result<Self> {
        // Read file
        let json = fs::read_to_string(path)
            .await
            .with_context(|| format!("Failed to read mutation state from {}", path.display()))?;

        // Deserialize from JSON
        let state = serde_json::from_str(&json).context("Failed to parse mutation state")?;

        Ok(state)
    }

    /// Get default state file path for a project
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
    pub fn default_state_path(project_path: &Path) -> PathBuf {
        project_path.join(".pmat").join("mutation_state.json")
    }

    /// Create backup of state before saving
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
    pub async fn save_with_backup(&self, path: &Path) -> Result<()> {
        // Create backup if file exists
        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())?;
        }

        // Save state
        self.save(path).await
    }
}

#[cfg(test)]
#[cfg(any())] // Tests disabled due to API changes (use cfg(any()) instead of custom cfg)
mod tests {
    use super::*;
    // Note: MutationOrigin is now merged into types::MutantOperator
    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));

        // Add first result
        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);

        // Add second result
        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<()> {
        // Create temp directory
        let temp_dir = TempDir::new()?;
        let state_path = temp_dir.path().join("mutation_state.json");

        // Create state
        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));

        // Save state
        state.save(&state_path).await?;

        // Verify file exists
        assert!(state_path.exists());

        // Load state
        let loaded_state = MutationState::load(&state_path).await?;

        // Verify state was loaded correctly
        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<()> {
        // Create temp directory
        let temp_dir = TempDir::new()?;
        let state_path = temp_dir.path().join("mutation_state.json");

        // Create initial state
        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));

        // Save initial state
        initial_state.save(&state_path).await?;

        // Create updated state
        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);

        // Save updated state with backup
        updated_state.save_with_backup(&state_path).await?;

        // Verify backup exists
        let backup_path = state_path.with_extension("json.bak");
        assert!(backup_path.exists());

        // Load backup
        let backup_json = fs::read_to_string(backup_path)?;
        let backup_state: MutationState = serde_json::from_str(&backup_json)?;

        // Verify backup contains initial state
        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);

        // Load updated state
        let updated_json = fs::read_to_string(&state_path)?;
        let loaded_updated_state: MutationState = serde_json::from_str(&updated_json)?;

        // Verify updated state was saved correctly
        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(())
    }
}