#![cfg_attr(coverage_nightly, coverage(off))]
use anyhow::Result;
use rayon::prelude::*;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Instant;
use walkdir::WalkDir;
use super::language_detection::WasmLanguageDetector;
#[derive(Debug, Clone)]
pub struct ParallelConfig {
pub thread_count: usize,
pub chunk_size: usize,
pub max_depth: usize,
pub sequential_threshold: usize,
pub enable_progress: bool,
}
impl Default for ParallelConfig {
fn default() -> Self {
Self {
thread_count: 0, chunk_size: 100,
max_depth: 10,
sequential_threshold: 10 * 1_024 * 1_024, enable_progress: false,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FileAnalysisResult {
pub path: PathBuf,
pub size_bytes: u64,
pub parse_time_ms: u64,
pub errors: Vec<String>,
}
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct AggregatedAnalysis {
pub total_files: usize,
pub successful_analyses: usize,
pub failed_analyses: usize,
pub total_parse_time_ms: u64,
pub file_results: Vec<FileAnalysisResult>,
pub errors_by_type: HashMap<String, usize>,
}
pub struct ParallelWasmAnalyzer {
_config: ParallelConfig,
_detector: WasmLanguageDetector,
}
impl ParallelWasmAnalyzer {
#[must_use]
pub fn new(config: ParallelConfig) -> Self {
Self {
_config: config,
_detector: WasmLanguageDetector::new(),
}
}
pub async fn analyze_directory(&self, dir_path: &Path) -> Result<AggregatedAnalysis> {
let _start_time = Instant::now();
let mut aggregated = AggregatedAnalysis::default();
let files: Vec<PathBuf> = WalkDir::new(dir_path)
.max_depth(self._config.max_depth)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| entry.file_type().is_file())
.map(|entry| entry.path().to_path_buf())
.filter(|path| self.is_relevant_file(path))
.collect();
aggregated.total_files = files.len();
let results: Vec<FileAnalysisResult> = files
.into_par_iter()
.map(|path| self.analyze_file(&path))
.collect();
for result in results {
if result.errors.is_empty() {
aggregated.successful_analyses += 1;
} else {
aggregated.failed_analyses += 1;
for error in &result.errors {
*aggregated.errors_by_type.entry(error.clone()).or_insert(0) += 1;
}
}
aggregated.total_parse_time_ms += result.parse_time_ms;
aggregated.file_results.push(result);
}
Ok(aggregated)
}
fn analyze_file(&self, path: &Path) -> FileAnalysisResult {
let start_time = Instant::now();
let mut errors = Vec::new();
let mut size_bytes = 0;
if let Ok(metadata) = std::fs::metadata(path) {
size_bytes = metadata.len();
} else {
errors.push("Failed to read file metadata".to_string());
}
if let Err(e) = std::fs::read_to_string(path) {
errors.push(format!("Failed to read file: {e}"));
}
let parse_time_ms = start_time.elapsed().as_millis() as u64;
FileAnalysisResult {
path: path.to_path_buf(),
size_bytes,
parse_time_ms,
errors,
}
}
fn is_relevant_file(&self, path: &Path) -> bool {
if let Some(extension) = path.extension() {
matches!(extension.to_str(), Some("wasm" | "wat" | "ts"))
} else {
false
}
}
}
impl Default for ParallelWasmAnalyzer {
fn default() -> Self {
Self::new(ParallelConfig::default())
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[tokio::test]
async fn test_parallel_analyzer() {
let analyzer = ParallelWasmAnalyzer::default();
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("test.wasm"), b"\0asm\x01\x00\x00\x00").unwrap();
fs::write(temp_dir.path().join("test.wat"), "(module)").unwrap();
let result = analyzer.analyze_directory(temp_dir.path()).await;
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.total_files, 2);
}
#[test]
fn test_file_relevance() {
let analyzer = ParallelWasmAnalyzer::default();
assert!(analyzer.is_relevant_file(Path::new("test.wasm")));
assert!(analyzer.is_relevant_file(Path::new("test.wat")));
assert!(analyzer.is_relevant_file(Path::new("test.ts")));
assert!(!analyzer.is_relevant_file(Path::new("test.txt")));
}
#[test]
fn test_parallel_config_default() {
let config = ParallelConfig::default();
assert_eq!(config.thread_count, 0);
assert_eq!(config.chunk_size, 100);
assert_eq!(config.max_depth, 10);
assert_eq!(config.sequential_threshold, 10 * 1_024 * 1_024);
assert!(!config.enable_progress);
}
#[test]
fn test_parallel_config_clone() {
let config = ParallelConfig {
thread_count: 4,
chunk_size: 50,
max_depth: 5,
sequential_threshold: 1024,
enable_progress: true,
};
let cloned = config.clone();
assert_eq!(cloned.thread_count, 4);
assert_eq!(cloned.chunk_size, 50);
assert!(cloned.enable_progress);
}
#[test]
fn test_parallel_analyzer_new_with_config() {
let config = ParallelConfig {
thread_count: 2,
chunk_size: 10,
max_depth: 3,
sequential_threshold: 1024,
enable_progress: true,
};
let analyzer = ParallelWasmAnalyzer::new(config);
assert_eq!(analyzer._config.thread_count, 2);
assert_eq!(analyzer._config.max_depth, 3);
}
#[test]
fn test_file_analysis_result() {
let result = FileAnalysisResult {
path: PathBuf::from("test.wasm"),
size_bytes: 1024,
parse_time_ms: 10,
errors: vec!["test error".to_string()],
};
assert_eq!(result.size_bytes, 1024);
assert_eq!(result.errors.len(), 1);
}
#[test]
fn test_aggregated_analysis_default() {
let analysis = AggregatedAnalysis::default();
assert_eq!(analysis.total_files, 0);
assert_eq!(analysis.successful_analyses, 0);
assert_eq!(analysis.failed_analyses, 0);
assert!(analysis.file_results.is_empty());
assert!(analysis.errors_by_type.is_empty());
}
#[test]
fn test_file_relevance_no_extension() {
let analyzer = ParallelWasmAnalyzer::default();
assert!(!analyzer.is_relevant_file(Path::new("Makefile")));
assert!(!analyzer.is_relevant_file(Path::new("README")));
}
#[tokio::test]
async fn test_analyze_empty_directory() {
let analyzer = ParallelWasmAnalyzer::default();
let temp_dir = TempDir::new().unwrap();
let result = analyzer.analyze_directory(temp_dir.path()).await;
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.total_files, 0);
assert_eq!(analysis.successful_analyses, 0);
}
#[tokio::test]
async fn test_analyze_directory_with_subdirs() {
let analyzer = ParallelWasmAnalyzer::default();
let temp_dir = TempDir::new().unwrap();
let sub_dir = temp_dir.path().join("subdir");
fs::create_dir(&sub_dir).unwrap();
fs::write(sub_dir.join("nested.wat"), "(module)").unwrap();
let result = analyzer.analyze_directory(temp_dir.path()).await;
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.total_files, 1);
}
#[test]
fn test_analyze_file_nonexistent() {
let analyzer = ParallelWasmAnalyzer::default();
let result = analyzer.analyze_file(Path::new("/nonexistent/file.wasm"));
assert!(!result.errors.is_empty());
}
#[test]
fn test_file_analysis_result_serialization() {
let result = FileAnalysisResult {
path: PathBuf::from("test.wasm"),
size_bytes: 512,
parse_time_ms: 5,
errors: vec![],
};
let json = serde_json::to_string(&result).unwrap();
let deserialized: FileAnalysisResult = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.size_bytes, 512);
}
#[test]
fn test_aggregated_analysis_serialization() {
let mut analysis = AggregatedAnalysis::default();
analysis.total_files = 5;
analysis.successful_analyses = 4;
analysis.failed_analyses = 1;
let json = serde_json::to_string(&analysis).unwrap();
let deserialized: AggregatedAnalysis = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.total_files, 5);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}