diff --git a/Cargo.toml b/Cargo.toml index 9501598..39fe23a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,4 +46,5 @@ expect_used = "warn" [[bench]] name = "benchmark" +path = "benches/benchmark.rs" harness = false diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 55f65e2..dfec464 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -1,145 +1,23 @@ -use anyhow::Result; +mod common; +mod scenarios; + +use common::setup::init_temp_dir; use criterion::{Criterion, criterion_group, criterion_main}; -use csv::Reader; -use serde::{Deserialize, Deserializer}; -use std::{ - fs::{File, create_dir_all}, - path::{Path, PathBuf}, - process::Command, - str::FromStr, - u64, +use scenarios::{ + basic::benchmark_basic, edge_cases::benchmark_edge_cases, + specific::benchmark_specific_scenarios, }; -use tempfile::TempDir; +use std::time::Duration; -const BASE_DIR: &str = env!("CARGO_MANIFEST_DIR"); - -#[allow(dead_code)] -#[derive(Debug, Clone, Default, Deserialize)] -struct Size(u64); - -#[allow(dead_code)] -#[derive(Debug, Clone, Default, Deserialize)] -struct Modified(u64); - -#[allow(dead_code)] -#[derive(Debug, Clone, Deserialize)] -struct Permissions(u32); - -impl Default for Permissions { - fn default() -> Self { - Self(644) - } -} - -#[derive(Debug, Clone, PartialEq)] -enum EntryType { - Dir, - File, - Symlink, - Other(String), -} - -impl FromStr for EntryType { - type Err = String; - - fn from_str(s: &str) -> std::result::Result { - match s { - "dir" => Ok(Self::Dir), - "file" => Ok(Self::File), - "symlink" => Ok(Self::Symlink), - other if other.is_empty() => Err("Empty entry type".to_string()), - other => Ok(Self::Other(other.into())), - } - } -} - -impl<'de> Deserialize<'de> for EntryType { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Self::from_str(&s).map_err(serde::de::Error::custom) - } -} - -#[allow(dead_code)] -#[derive(Debug, Clone, Deserialize)] -struct FileEntry { - #[serde(rename = "type")] - entry_type: EntryType, - directory: PathBuf, - path: PathBuf, - #[serde(default)] - size: Size, - #[serde(default)] - modified: Modified, - #[serde(default)] - permissions: Permissions, -} - -impl FileEntry { - fn to_tempfile(&self, base: &Path) -> Result<()> { - let full_path = base.join(&self.path); - match self.entry_type { - EntryType::Dir => create_dir(&full_path), - EntryType::File => create_file(&full_path), - EntryType::Symlink => Ok(()), - EntryType::Other(_) => Ok(()), - } - } -} - -fn create_file(path: &Path) -> Result<()> { - if let Some(parent) = path.parent() { - create_dir(parent)?; - } - File::create(path)?; - Ok(()) -} - -fn create_dir(path: &Path) -> Result<()> { - create_dir_all(path)?; - Ok(()) -} - -fn process_directory(path: &Path) { - let binary_path = PathBuf::from(BASE_DIR).join("target/release/project-finder"); - Command::new(binary_path) - .arg(path) - .output() - .expect("failed to run binary"); -} - -fn setup_entries() -> Result { - let temp_dir = TempDir::new()?; - println!("Temporary directory: {:?}", temp_dir.path()); - - let csv_path = PathBuf::from(BASE_DIR) - .join("benches/fixtures") - .join("snapshot-2025-03-31_09-20-03.csv"); - let mut rdr = Reader::from_path(csv_path)?; - - rdr.deserialize::() - .for_each(|entry| match entry { - Ok(entry) => { - if let Err(e) = entry.to_tempfile(temp_dir.path()) { - // eprintln!("Error processing entry: {}", e); - } - } - Err(e) => eprintln!("Failed to deserialize entry: {}", e), - }); - - Ok(temp_dir) -} - -fn benchmark_processing(c: &mut Criterion) { - let temp_dir = setup_entries().expect("Failed to setup file entries"); - - c.bench_function("process_directory", |b| { - b.iter(|| process_directory(temp_dir.path())) - }); -} - -criterion_group!(benches, benchmark_processing); +criterion_group!( + name = benches; + config = { + let c = Criterion::default() + .sample_size(10) + .measurement_time(Duration::from_secs(30)); + init_temp_dir(); + c + }; + targets = benchmark_basic, benchmark_edge_cases, benchmark_specific_scenarios +); criterion_main!(benches); diff --git a/benches/common/mod.rs b/benches/common/mod.rs new file mode 100644 index 0000000..23f5525 --- /dev/null +++ b/benches/common/mod.rs @@ -0,0 +1,2 @@ +pub mod setup; +pub mod utils; diff --git a/benches/common/setup.rs b/benches/common/setup.rs new file mode 100644 index 0000000..e855fe2 --- /dev/null +++ b/benches/common/setup.rs @@ -0,0 +1,188 @@ +use crate::common::utils::BASE_DIR; +use anyhow; +use csv::Reader; +use regex::Regex; +use serde::{Deserialize, Deserializer}; +use std::{ + fs::{self, File, create_dir_all}, + path::{Path, PathBuf}, + str::FromStr, + sync::OnceLock, +}; +use tempfile::TempDir; + +pub static TEMP_DIR: OnceLock = OnceLock::new(); + +pub fn init_temp_dir() { + TEMP_DIR.get_or_init(|| setup_entries().expect("Failed to setup test directory")); +} + +#[derive(Debug, Clone)] +pub struct BenchParams { + pub depth: usize, + pub max_results: usize, + pub verbose: bool, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Deserialize)] +struct FileEntry { + #[serde(rename = "type")] + entry_type: EntryType, + directory: PathBuf, + path: PathBuf, + #[serde(default)] + size: Size, + #[serde(default)] + modified: Modified, + #[serde(default)] + permissions: Permissions, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Default, Deserialize)] +struct Size(#[serde(deserialize_with = "deserialize_u64_from_empty")] u64); + +#[allow(dead_code)] +#[derive(Debug, Clone, Default, Deserialize)] +struct Modified(#[serde(deserialize_with = "deserialize_u64_from_empty")] u64); + +#[allow(dead_code)] +#[derive(Debug, Clone, Deserialize)] +struct Permissions(#[serde(deserialize_with = "deserialize_u16_from_empty")] u16); + +pub fn setup_entries() -> anyhow::Result { + let temp_dir = TempDir::new()?; + println!("Temporary directory: {:?}", temp_dir.path()); + + let fixtures_dir = PathBuf::from(BASE_DIR).join("benches/fixtures"); + + let snapshot_path = last_snaphow_file(&fixtures_dir)?; + + let mut rdr = Reader::from_path(snapshot_path)?; + + rdr.deserialize::() + .for_each(|entry| match entry { + Ok(entry) => { + if let Err(e) = entry.to_tempfile(temp_dir.path()) { + eprintln!("Error processing entry: {}", e); + } + } + Err(e) => eprintln!("Failed to deserialize entry: {}", e), + }); + + Ok(temp_dir) +} + +fn last_snaphow_file(dir: &Path) -> anyhow::Result { + let re = Regex::new(r"^snapshot-(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.csv$")?; + let mut snapshots = fs::read_dir(dir)? + .filter_map(|entry| { + entry.ok().and_then(|entry| { + let file_name = entry.file_name(); + + if let Some(caps) = re.captures(&file_name.to_string_lossy()) { + let [y, m, d, h, min, s] = (1..=6) + .filter_map(|i| caps.get(i)?.as_str().parse().ok()) + .collect::>() + .try_into() + .ok()?; + return Some(((y, m, d, h, min, s), entry.path())); + } + None + }) + }) + .collect::>(); + + snapshots.sort_by_key(|(timestamp, _)| *timestamp); + + snapshots + .last() + .map(|(_, path)| path.clone()) + .ok_or_else(|| anyhow::anyhow!("No snapshot files found in directory")) +} + +fn deserialize_u64_from_empty<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + if s.trim().is_empty() { + return Ok(0); + } + s.parse().map_err(serde::de::Error::custom) +} + +fn deserialize_u16_from_empty<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + if s.trim().is_empty() { + return Ok(644); + } + s.parse().map_err(serde::de::Error::custom) +} + +impl Default for Permissions { + fn default() -> Self { + Self(644) + } +} + +#[derive(Debug, Clone, PartialEq)] +enum EntryType { + Dir, + File, + Symlink, + Other(String), +} + +impl FromStr for EntryType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "dir" => Ok(Self::Dir), + "file" => Ok(Self::File), + "symlink" => Ok(Self::Symlink), + other if other.is_empty() => Err("Empty entry type".to_string()), + other => Ok(Self::Other(other.into())), + } + } +} + +impl<'de> Deserialize<'de> for EntryType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl FileEntry { + fn to_tempfile(&self, base: &Path) -> anyhow::Result<()> { + let full_path = base.join(&self.path); + match self.entry_type { + EntryType::Dir => create_dir(&full_path), + EntryType::File => create_file(&full_path), + EntryType::Symlink => Ok(()), + EntryType::Other(_) => Ok(()), + } + } +} + +fn create_file(path: &Path) -> anyhow::Result<()> { + if let Some(parent) = path.parent() { + create_dir(parent)?; + } + File::create(path)?; + Ok(()) +} + +fn create_dir(path: &Path) -> anyhow::Result<()> { + create_dir_all(path)?; + Ok(()) +} diff --git a/benches/common/utils.rs b/benches/common/utils.rs new file mode 100644 index 0000000..674646a --- /dev/null +++ b/benches/common/utils.rs @@ -0,0 +1,62 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +pub const BASE_DIR: &str = env!("CARGO_MANIFEST_DIR"); + +pub fn run_binary_with_args( + path: &Path, + depth: usize, + max_results: usize, + verbose: bool, +) -> anyhow::Result<()> { + let binary_path = PathBuf::from(BASE_DIR).join("target/release/project-finder"); + + if !binary_path.exists() { + return Err(anyhow::anyhow!( + "Binary not found at {}. Did you run `cargo build --release`?", + binary_path.display() + )); + } + + let mut cmd = Command::new(&binary_path); + + // Add the path to search + cmd.arg(path); + + // Add depth parameter + cmd.arg("--depth").arg(depth.to_string()); + + // Add max_results parameter if not zero + if max_results > 0 { + cmd.arg("--max-results").arg(max_results.to_string()); + } + + // Add verbose flag if true + if verbose { + cmd.arg("--verbose"); + } + + let output = cmd.output().map_err(|e| { + anyhow::anyhow!("Failed to execute binary {}: {}", binary_path.display(), e) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!( + "Process failed with status: {}\nStderr: {}", + output.status, + stderr + )); + } + + Ok(()) +} +pub fn create_deep_directory(base: &Path, depth: usize) -> anyhow::Result<()> { + todo!() +} + +pub fn create_wide_directory(base: &Path, width: usize) -> anyhow::Result<()> { + todo!() +} diff --git a/benches/scenarios/basic.rs b/benches/scenarios/basic.rs new file mode 100644 index 0000000..6afc656 --- /dev/null +++ b/benches/scenarios/basic.rs @@ -0,0 +1,60 @@ +use crate::common::{ + setup::{BenchParams, TEMP_DIR, init_temp_dir, setup_entries}, + utils::{BASE_DIR, run_binary_with_args}, +}; +use criterion::{BenchmarkId, Criterion}; +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +fn process_directory(path: &Path) { + let binary_path = PathBuf::from(BASE_DIR).join("target/release/project-finder"); + Command::new(binary_path) + .arg(path) + .output() + .expect("failed to run binary"); +} + +pub fn benchmark_basic(c: &mut Criterion) { + init_temp_dir(); + let temp_dir = TEMP_DIR.get().unwrap().path(); + + let params = vec![ + BenchParams { + depth: 1, + max_results: 0, + verbose: false, + }, + BenchParams { + depth: 5, + max_results: 0, + verbose: false, + }, + ]; + + let mut group = c.benchmark_group("basic_scenarios"); + + group.bench_function("process_directory", |b| { + b.iter(|| process_directory(temp_dir)) + }); + + for param in params { + let id = BenchmarkId::new( + format!( + "depth{}_max{}_verbose{}", + param.depth, param.max_results, param.verbose + ), + param.depth, + ); + + group.bench_with_input(id, ¶m, |b, param| { + b.iter(|| { + run_binary_with_args(temp_dir, param.depth, param.max_results, param.verbose) + .expect("Failed to run binary") + }) + }); + } + + group.finish(); +} diff --git a/benches/scenarios/edge_cases.rs b/benches/scenarios/edge_cases.rs new file mode 100644 index 0000000..b56c2b2 --- /dev/null +++ b/benches/scenarios/edge_cases.rs @@ -0,0 +1,6 @@ +use criterion::Criterion; + +pub fn benchmark_edge_cases(c: &mut Criterion) { + let mut group = c.benchmark_group("edge_cases"); + group.finish(); +} diff --git a/benches/scenarios/mod.rs b/benches/scenarios/mod.rs new file mode 100644 index 0000000..334c6b2 --- /dev/null +++ b/benches/scenarios/mod.rs @@ -0,0 +1,3 @@ +pub mod basic; +pub mod edge_cases; +pub mod specific; diff --git a/benches/scenarios/specific.rs b/benches/scenarios/specific.rs new file mode 100644 index 0000000..0542f05 --- /dev/null +++ b/benches/scenarios/specific.rs @@ -0,0 +1,6 @@ +use criterion::Criterion; + +pub fn benchmark_specific_scenarios(c: &mut Criterion) { + let mut group = c.benchmark_group("specific_scenarios"); + group.finish(); +}