pub mod analysis_types;
pub mod comparison;
pub mod error;
pub mod executor;
pub mod parser;
pub mod recommendation;
pub mod recommendations;
pub use analysis_types::*;
pub use comparison::*;
pub use error::*;
pub use recommendation::*;
use crate::infra::{CommandExecutor, FileSystem, RealCommandExecutor, RealFileSystem};
use std::path::Path;
pub struct TwiggyAnalyzer<
FS: FileSystem = RealFileSystem,
CE: CommandExecutor = RealCommandExecutor,
> {
wasm_file: std::path::PathBuf,
fs: FS,
cmd_executor: CE,
}
impl TwiggyAnalyzer {
pub fn new(wasm_file: impl Into<std::path::PathBuf>) -> Self {
Self::with_executors(wasm_file, RealFileSystem, RealCommandExecutor)
}
pub fn check_installation() -> Result<bool, TwiggyAnalysisError> {
Self::check_installation_with_executor(&RealCommandExecutor)
}
pub fn installation_instructions() -> &'static str {
"twiggy is not installed. Install it with:\n cargo install twiggy"
}
pub fn compare<FS: FileSystem, CE: CommandExecutor>(
before: &Path,
after: &Path,
fs: &FS,
cmd_executor: &CE,
) -> Result<ComparisonResults, TwiggyAnalysisError> {
let before_size_bytes = fs.metadata(before)?.len();
let after_size_bytes = fs.metadata(after)?.len();
let delta_bytes = after_size_bytes as i64 - before_size_bytes as i64;
let delta_percent = (delta_bytes as f64 / before_size_bytes as f64) * 100.0;
let before = before.to_path_buf();
let after = after.to_path_buf();
let output =
cmd_executor.execute(|cmd| cmd.args(["diff"]).arg(&before).arg(&after), "twiggy")?;
let diff_output = String::from_utf8_lossy(&output.stdout);
let top_changes = Self::parse_diff_output(&diff_output)?;
Ok(ComparisonResults {
before_size_bytes,
after_size_bytes,
delta_bytes,
delta_percent,
top_changes,
})
}
}
impl<FS: FileSystem, CE: CommandExecutor> TwiggyAnalyzer<FS, CE> {
pub fn with_executors(
wasm_file: impl Into<std::path::PathBuf>,
fs: FS,
cmd_executor: CE,
) -> Self {
Self {
wasm_file: wasm_file.into(),
fs,
cmd_executor,
}
}
#[deprecated(since = "0.1.0", note = "Use with_executors instead")]
pub fn with_executor(wasm_file: impl Into<std::path::PathBuf>, cmd_executor: CE) -> Self
where
FS: Default,
{
Self::with_executors(wasm_file, FS::default(), cmd_executor)
}
pub fn check_installation_with_executor<E: CommandExecutor>(
executor: &E,
) -> Result<bool, TwiggyAnalysisError> {
match executor.execute(|cmd| cmd.arg("--version"), "twiggy") {
Ok(output) => Ok(output.status.success()),
Err(_) => Ok(false),
}
}
#[must_use = "Analysis results should be used or printed"]
pub fn analyze(&self, mode: AnalysisMode) -> Result<AnalysisResults, TwiggyAnalysisError> {
if !self.wasm_file.exists() {
return Err(TwiggyAnalysisError::WasmFileNotFound(
self.wasm_file.display().to_string(),
));
}
let total_size_bytes = self.fs.metadata(&self.wasm_file)?.len();
let output = match mode {
AnalysisMode::Top => self.run_top_analysis()?,
AnalysisMode::Dominators => self.run_dominators_analysis()?,
AnalysisMode::Dead => self.run_dead_code_analysis()?,
AnalysisMode::Monos => self.run_monos_analysis()?,
};
let items = self.parse_output(&output, mode)?;
let mono_groups = if matches!(mode, AnalysisMode::Monos) {
Some(self.group_monomorphizations(&items))
} else {
None
};
let recommendations = if let Some(ref groups) = mono_groups {
self.generate_monos_recommendations_enhanced(groups, total_size_bytes)
} else {
self.generate_recommendations(&items, total_size_bytes, mode)
};
Ok(AnalysisResults {
total_size_bytes,
mode: format!("{:?}", mode).to_lowercase(),
items,
recommendations,
mono_groups,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_installation_twiggy_availability_succeeds() {
let result = TwiggyAnalyzer::check_installation();
assert!(result.is_ok());
}
#[test]
fn test_analyze_nonexistent_file_returns_file_error() {
let analyzer = TwiggyAnalyzer::new("nonexistent-file.wasm");
let result = analyzer.analyze(AnalysisMode::Top);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("not found") || err.to_string().contains("WASM file"));
}
#[test]
fn test_compare_nonexistent_files_returns_file_error() {
use crate::infra::{RealCommandExecutor, RealFileSystem};
let result = TwiggyAnalyzer::compare(
Path::new("nonexistent_before.wasm"),
Path::new("nonexistent_after.wasm"),
&RealFileSystem,
&RealCommandExecutor,
);
assert!(result.is_err());
}
#[test]
fn test_installation_instructions_provides_cargo_command() {
let instructions = TwiggyAnalyzer::installation_instructions();
assert!(!instructions.is_empty());
assert!(instructions.contains("twiggy") || instructions.contains("cargo install"));
}
#[test]
fn test_analyze_invalid_wasm_file_handles_corrupted_data() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"NOT_WASM_DATA").unwrap();
temp_file.flush().unwrap();
let analyzer = TwiggyAnalyzer::new(temp_file.path());
let result = analyzer.analyze(AnalysisMode::Top);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, TwiggyAnalysisError::CommandFailed(_, _))
|| matches!(err, TwiggyAnalysisError::Io(_))
);
}
#[test]
fn test_analyze_empty_wasm_file_handles_edge_case() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"").unwrap();
temp_file.flush().unwrap();
let analyzer = TwiggyAnalyzer::new(temp_file.path());
let result = analyzer.analyze(AnalysisMode::Top);
assert!(result.is_err());
}
#[test]
fn test_analyze_with_unreadable_file_returns_error() {
#[cfg(unix)]
{
use std::fs;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
temp_file.as_file().write_all(b"test").unwrap();
let path = temp_file.path().to_path_buf();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o000);
fs::set_permissions(&path, perms).unwrap();
let analyzer = TwiggyAnalyzer::new(&path);
let result = analyzer.analyze(AnalysisMode::Top);
assert!(result.is_err());
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o644);
let _ = fs::set_permissions(&path, perms);
}
}
#[test]
fn test_compare_with_invalid_wasm_does_not_panic() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut before_file = NamedTempFile::new().unwrap();
before_file.write_all(b"INVALID").unwrap();
before_file.flush().unwrap();
let mut after_file = NamedTempFile::new().unwrap();
after_file.write_all(b"ALSO_INVALID").unwrap();
after_file.flush().unwrap();
use crate::infra::{RealCommandExecutor, RealFileSystem};
let result = TwiggyAnalyzer::compare(
before_file.path(),
after_file.path(),
&RealFileSystem,
&RealCommandExecutor,
);
let _ = result; }
#[test]
fn test_analyze_top_mode_with_invalid_wasm_returns_error() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"INVALID_WASM").unwrap();
temp_file.flush().unwrap();
let analyzer = TwiggyAnalyzer::new(temp_file.path());
let result = analyzer.analyze(AnalysisMode::Top);
assert!(result.is_err());
}
#[test]
fn test_analyze_dominators_mode_with_invalid_wasm_returns_error() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"INVALID_WASM").unwrap();
temp_file.flush().unwrap();
let analyzer = TwiggyAnalyzer::new(temp_file.path());
let result = analyzer.analyze(AnalysisMode::Dominators);
assert!(result.is_err());
}
#[test]
fn test_analyze_dead_mode_with_invalid_wasm_returns_error() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"INVALID_WASM").unwrap();
temp_file.flush().unwrap();
let analyzer = TwiggyAnalyzer::new(temp_file.path());
let result = analyzer.analyze(AnalysisMode::Dead);
assert!(result.is_err());
}
#[test]
fn test_analyze_monos_mode_with_invalid_wasm_returns_error() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"INVALID_WASM").unwrap();
temp_file.flush().unwrap();
let analyzer = TwiggyAnalyzer::new(temp_file.path());
let result = analyzer.analyze(AnalysisMode::Monos);
assert!(result.is_err());
}
}