mobench_sdk/builders/
common.rs

1//! Common utilities shared between Android and iOS builders.
2//!
3//! This module provides helper functions that are used by both [`super::AndroidBuilder`]
4//! and [`super::IosBuilder`] to ensure consistent behavior and error handling.
5//!
6//! ## Features
7//!
8//! - **Workspace-aware target detection** - Correctly handles Cargo workspaces where
9//!   the target directory is at the workspace root
10//! - **Host library resolution** - Finds compiled libraries for UniFFI binding generation
11//! - **Consistent error handling** - All errors include actionable fix suggestions
12//!
13//! ## Error Messages
14//!
15//! All functions in this module provide detailed, actionable error messages that include:
16//! - What went wrong
17//! - Where it happened (paths, commands)
18//! - How to fix it (specific commands or configuration changes)
19
20use std::env;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23
24use crate::types::BenchError;
25
26/// Validates that the project root is a valid directory for building.
27///
28/// This function checks that:
29/// - The path exists
30/// - The path is a directory
31/// - The directory contains a Cargo.toml file (or has a crate directory with one)
32///
33/// # Arguments
34/// * `project_root` - The project root directory to validate
35/// * `crate_name` - The name of the crate being built (used to check crate directories)
36///
37/// # Returns
38/// `Ok(())` if validation passes, or a descriptive `BenchError` if it fails.
39pub fn validate_project_root(project_root: &Path, crate_name: &str) -> Result<(), BenchError> {
40    // Check if path exists
41    if !project_root.exists() {
42        return Err(BenchError::Build(format!(
43            "Project root does not exist: {}\n\n\
44             Ensure you are running from the correct directory or specify --project-root.",
45            project_root.display()
46        )));
47    }
48
49    // Check if path is a directory
50    if !project_root.is_dir() {
51        return Err(BenchError::Build(format!(
52            "Project root is not a directory: {}\n\n\
53             Expected a directory containing your Rust project.",
54            project_root.display()
55        )));
56    }
57
58    // Check for Cargo.toml in project root or standard crate locations
59    let root_cargo = project_root.join("Cargo.toml");
60    let bench_mobile_cargo = project_root.join("bench-mobile/Cargo.toml");
61    let crates_cargo = project_root.join(format!("crates/{}/Cargo.toml", crate_name));
62
63    if !root_cargo.exists() && !bench_mobile_cargo.exists() && !crates_cargo.exists() {
64        return Err(BenchError::Build(format!(
65            "No Cargo.toml found in project root or expected crate locations.\n\n\
66             Searched:\n\
67             - {}\n\
68             - {}\n\
69             - {}\n\n\
70             Ensure you are in a Rust project directory or use --crate-path to specify the crate location.",
71            root_cargo.display(),
72            bench_mobile_cargo.display(),
73            crates_cargo.display()
74        )));
75    }
76
77    Ok(())
78}
79
80/// Detects the actual Cargo target directory using `cargo metadata`.
81///
82/// This correctly handles Cargo workspaces where the target directory
83/// is at the workspace root, not the crate directory.
84///
85/// # Arguments
86/// * `crate_dir` - Path to the crate directory containing Cargo.toml
87///
88/// # Returns
89/// The path to the target directory, or falls back to `crate_dir/target` if detection fails.
90///
91/// # Warnings
92/// Prints a warning to stderr if falling back to the default target directory due to
93/// cargo metadata failures or parsing issues.
94pub fn get_cargo_target_dir(crate_dir: &Path) -> Result<PathBuf, BenchError> {
95    let output = Command::new("cargo")
96        .args(["metadata", "--format-version", "1", "--no-deps"])
97        .current_dir(crate_dir)
98        .output()
99        .map_err(|e| {
100            BenchError::Build(format!(
101                "Failed to run cargo metadata.\n\n\
102                 Working directory: {}\n\
103                 Error: {}\n\n\
104                 Ensure cargo is installed and on PATH.",
105                crate_dir.display(),
106                e
107            ))
108        })?;
109
110    if !output.status.success() {
111        // Fall back to crate_dir/target if cargo metadata fails
112        let fallback = crate_dir.join("target");
113        let stderr = String::from_utf8_lossy(&output.stderr);
114        eprintln!(
115            "Warning: cargo metadata failed (exit {}), falling back to {}.\n\
116             Stderr: {}\n\
117             This may cause build issues if you are in a Cargo workspace.",
118            output.status,
119            fallback.display(),
120            stderr.lines().take(3).collect::<Vec<_>>().join("\n")
121        );
122        return Ok(fallback);
123    }
124
125    let stdout = String::from_utf8_lossy(&output.stdout);
126
127    // Parse the JSON to extract target_directory
128    // Using simple string parsing to avoid adding serde_json dependency
129    if let Some(start) = stdout.find("\"target_directory\":\"") {
130        let rest = &stdout[start + 20..];
131        if let Some(end) = rest.find('"') {
132            let target_dir = &rest[..end];
133            // Handle escaped backslashes in Windows paths
134            let target_dir = target_dir.replace("\\\\", "\\");
135            return Ok(PathBuf::from(target_dir));
136        }
137    }
138
139    // Fall back to crate_dir/target if parsing fails
140    let fallback = crate_dir.join("target");
141    eprintln!(
142        "Warning: Failed to parse target_directory from cargo metadata output, \
143         falling back to {}.\n\
144         This may cause build issues if you are in a Cargo workspace.",
145        fallback.display()
146    );
147    Ok(fallback)
148}
149
150/// Finds the host library path for UniFFI binding generation.
151///
152/// UniFFI requires a host-compiled library to generate bindings. This function
153/// locates that library in the target directory.
154///
155/// # Arguments
156/// * `crate_dir` - Path to the crate directory
157/// * `crate_name` - Name of the crate (used to construct library filename)
158///
159/// # Returns
160/// Path to the host library (e.g., `libfoo.dylib` on macOS, `libfoo.so` on Linux)
161pub fn host_lib_path(crate_dir: &Path, crate_name: &str) -> Result<PathBuf, BenchError> {
162    let lib_prefix = if cfg!(target_os = "windows") {
163        ""
164    } else {
165        "lib"
166    };
167    let lib_ext = match env::consts::OS {
168        "macos" => "dylib",
169        "linux" => "so",
170        other => {
171            return Err(BenchError::Build(format!(
172                "Unsupported host OS for binding generation: {}\n\n\
173                 Supported platforms:\n\
174                 - macOS (generates .dylib)\n\
175                 - Linux (generates .so)\n\n\
176                 Windows is not currently supported for binding generation.",
177                other
178            )));
179        }
180    };
181
182    // Use cargo metadata to find the actual target directory
183    let target_dir = get_cargo_target_dir(crate_dir)?;
184
185    let lib_name = format!(
186        "{}{}.{}",
187        lib_prefix,
188        crate_name.replace('-', "_"),
189        lib_ext
190    );
191    let path = target_dir.join("debug").join(&lib_name);
192
193    if !path.exists() {
194        return Err(BenchError::Build(format!(
195            "Host library for UniFFI not found.\n\n\
196             Expected: {}\n\
197             Target directory: {}\n\n\
198             To fix this:\n\
199             1. Build the host library first:\n\
200                cargo build -p {}\n\n\
201             2. Ensure your crate produces a cdylib:\n\
202                [lib]\n\
203                crate-type = [\"cdylib\"]\n\n\
204             3. Check that the library name matches: {}",
205            path.display(),
206            target_dir.display(),
207            crate_name,
208            lib_name
209        )));
210    }
211    Ok(path)
212}
213
214/// Runs an external command with consistent error handling.
215///
216/// Captures both stdout and stderr on failure and formats them into
217/// an actionable error message.
218///
219/// # Arguments
220/// * `cmd` - The command to execute
221/// * `description` - Human-readable description of what the command does
222///
223/// # Returns
224/// `Ok(())` if the command succeeds, or a `BenchError` with detailed output on failure.
225pub fn run_command(mut cmd: Command, description: &str) -> Result<(), BenchError> {
226    let output = cmd.output().map_err(|e| {
227        BenchError::Build(format!(
228            "Failed to start {}.\n\n\
229             Error: {}\n\n\
230             Ensure the tool is installed and available on PATH.",
231            description, e
232        ))
233    })?;
234
235    if !output.status.success() {
236        let stdout = String::from_utf8_lossy(&output.stdout);
237        let stderr = String::from_utf8_lossy(&output.stderr);
238        return Err(BenchError::Build(format!(
239            "{} failed.\n\n\
240             Exit status: {}\n\n\
241             Stdout:\n{}\n\n\
242             Stderr:\n{}",
243            description, output.status, stdout, stderr
244        )));
245    }
246    Ok(())
247}
248
249/// Reads the package name from a Cargo.toml file.
250///
251/// This function parses the `[package]` section of a Cargo.toml and extracts
252/// the `name` field. It uses simple string parsing to avoid adding toml
253/// dependencies.
254///
255/// # Arguments
256/// * `cargo_toml_path` - Path to the Cargo.toml file
257///
258/// # Returns
259/// `Some(name)` if the package name is found, `None` otherwise.
260///
261/// # Example
262/// ```ignore
263/// let name = read_package_name(Path::new("/path/to/Cargo.toml"));
264/// if let Some(name) = name {
265///     println!("Package name: {}", name);
266/// }
267/// ```
268pub fn read_package_name(cargo_toml_path: &Path) -> Option<String> {
269    let content = std::fs::read_to_string(cargo_toml_path).ok()?;
270
271    // Find [package] section
272    let package_start = content.find("[package]")?;
273    let package_section = &content[package_start..];
274
275    // Find the end of the package section (next section or end of file)
276    let section_end = package_section[1..]
277        .find("\n[")
278        .map(|i| i + 1)
279        .unwrap_or(package_section.len());
280    let package_section = &package_section[..section_end];
281
282    // Find name = "..." or name = '...'
283    for line in package_section.lines() {
284        let trimmed = line.trim();
285        if trimmed.starts_with("name") {
286            // Parse: name = "value" or name = 'value'
287            if let Some(eq_pos) = trimmed.find('=') {
288                let value_part = trimmed[eq_pos + 1..].trim();
289                // Extract string value (handle both " and ')
290                let (quote_char, start) = if value_part.starts_with('"') {
291                    ('"', 1)
292                } else if value_part.starts_with('\'') {
293                    ('\'', 1)
294                } else {
295                    continue;
296                };
297                if let Some(end) = value_part[start..].find(quote_char) {
298                    return Some(value_part[start..start + end].to_string());
299                }
300            }
301        }
302    }
303
304    None
305}
306
307/// Embeds a bench spec JSON file into the Android assets and iOS bundle resources.
308///
309/// This function writes a `bench_spec.json` file to the appropriate location for
310/// both Android (assets directory) and iOS (bundle resources) so the mobile app
311/// can read the benchmark configuration at runtime.
312///
313/// # Arguments
314/// * `output_dir` - The mobench output directory (e.g., `target/mobench`)
315/// * `spec` - The benchmark specification as a JSON-serializable struct
316///
317/// # Example
318/// ```ignore
319/// use mobench_sdk::builders::common::embed_bench_spec;
320/// use mobench_sdk::BenchSpec;
321///
322/// let spec = BenchSpec {
323///     name: "my_crate::my_benchmark".to_string(),
324///     iterations: 100,
325///     warmup: 10,
326/// };
327///
328/// embed_bench_spec(Path::new("target/mobench"), &spec)?;
329/// ```
330pub fn embed_bench_spec<S: serde::Serialize>(output_dir: &Path, spec: &S) -> Result<(), BenchError> {
331    let spec_json = serde_json::to_string_pretty(spec).map_err(|e| {
332        BenchError::Build(format!("Failed to serialize bench spec: {}", e))
333    })?;
334
335    // Android: Write to assets directory
336    let android_assets_dir = output_dir.join("android/app/src/main/assets");
337    if output_dir.join("android").exists() {
338        std::fs::create_dir_all(&android_assets_dir).map_err(|e| {
339            BenchError::Build(format!(
340                "Failed to create Android assets directory at {}: {}",
341                android_assets_dir.display(),
342                e
343            ))
344        })?;
345        let android_spec_path = android_assets_dir.join("bench_spec.json");
346        std::fs::write(&android_spec_path, &spec_json).map_err(|e| {
347            BenchError::Build(format!(
348                "Failed to write Android bench spec to {}: {}",
349                android_spec_path.display(),
350                e
351            ))
352        })?;
353    }
354
355    // iOS: Write to Resources directory in the Xcode project
356    let ios_resources_dir = output_dir.join("ios/BenchRunner/BenchRunner/Resources");
357    if output_dir.join("ios/BenchRunner").exists() {
358        std::fs::create_dir_all(&ios_resources_dir).map_err(|e| {
359            BenchError::Build(format!(
360                "Failed to create iOS Resources directory at {}: {}",
361                ios_resources_dir.display(),
362                e
363            ))
364        })?;
365        let ios_spec_path = ios_resources_dir.join("bench_spec.json");
366        std::fs::write(&ios_spec_path, &spec_json).map_err(|e| {
367            BenchError::Build(format!(
368                "Failed to write iOS bench spec to {}: {}",
369                ios_spec_path.display(),
370                e
371            ))
372        })?;
373    }
374
375    Ok(())
376}
377
378/// Represents a benchmark specification for embedding.
379///
380/// This is a simple struct that can be serialized to JSON and embedded
381/// in mobile app bundles.
382#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
383pub struct EmbeddedBenchSpec {
384    /// The benchmark function name (e.g., "my_crate::my_benchmark")
385    pub function: String,
386    /// Number of benchmark iterations
387    pub iterations: u32,
388    /// Number of warmup iterations
389    pub warmup: u32,
390}
391
392/// Build metadata for artifact correlation and traceability.
393///
394/// This struct captures metadata about the build environment to enable
395/// reproducibility and debugging of benchmark results.
396#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
397pub struct BenchMeta {
398    /// Benchmark specification that was used
399    pub spec: EmbeddedBenchSpec,
400    /// Git commit hash (if in a git repository)
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub commit_hash: Option<String>,
403    /// Git branch name (if available)
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub branch: Option<String>,
406    /// Whether the git working directory was dirty
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub dirty: Option<bool>,
409    /// Build timestamp in RFC3339 format
410    pub build_time: String,
411    /// Build timestamp as Unix epoch seconds
412    pub build_time_unix: u64,
413    /// Target platform ("android" or "ios")
414    pub target: String,
415    /// Build profile ("debug" or "release")
416    pub profile: String,
417    /// mobench version
418    pub mobench_version: String,
419    /// Rust version used for the build
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub rust_version: Option<String>,
422    /// Host OS (e.g., "macos", "linux")
423    pub host_os: String,
424}
425
426/// Gets the current git commit hash (short form).
427pub fn get_git_commit() -> Option<String> {
428    let output = Command::new("git")
429        .args(["rev-parse", "--short", "HEAD"])
430        .output()
431        .ok()?;
432
433    if output.status.success() {
434        let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
435        if !hash.is_empty() {
436            return Some(hash);
437        }
438    }
439    None
440}
441
442/// Gets the current git branch name.
443pub fn get_git_branch() -> Option<String> {
444    let output = Command::new("git")
445        .args(["rev-parse", "--abbrev-ref", "HEAD"])
446        .output()
447        .ok()?;
448
449    if output.status.success() {
450        let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
451        if !branch.is_empty() && branch != "HEAD" {
452            return Some(branch);
453        }
454    }
455    None
456}
457
458/// Checks if the git working directory has uncommitted changes.
459pub fn is_git_dirty() -> Option<bool> {
460    let output = Command::new("git")
461        .args(["status", "--porcelain"])
462        .output()
463        .ok()?;
464
465    if output.status.success() {
466        let status = String::from_utf8_lossy(&output.stdout);
467        Some(!status.trim().is_empty())
468    } else {
469        None
470    }
471}
472
473/// Gets the Rust version.
474pub fn get_rust_version() -> Option<String> {
475    let output = Command::new("rustc")
476        .args(["--version"])
477        .output()
478        .ok()?;
479
480    if output.status.success() {
481        let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
482        if !version.is_empty() {
483            return Some(version);
484        }
485    }
486    None
487}
488
489/// Creates a BenchMeta instance with current build information.
490pub fn create_bench_meta(spec: &EmbeddedBenchSpec, target: &str, profile: &str) -> BenchMeta {
491    use std::time::{SystemTime, UNIX_EPOCH};
492
493    let now = SystemTime::now()
494        .duration_since(UNIX_EPOCH)
495        .unwrap_or_default();
496
497    // Format as RFC3339
498    let build_time = {
499        let secs = now.as_secs();
500        // Simple UTC timestamp formatting
501        let days_since_epoch = secs / 86400;
502        let remaining_secs = secs % 86400;
503        let hours = remaining_secs / 3600;
504        let minutes = (remaining_secs % 3600) / 60;
505        let seconds = remaining_secs % 60;
506
507        // Calculate year, month, day from days since epoch (1970-01-01)
508        // Simplified calculation - good enough for build metadata
509        let (year, month, day) = days_to_ymd(days_since_epoch);
510
511        format!(
512            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
513            year, month, day, hours, minutes, seconds
514        )
515    };
516
517    BenchMeta {
518        spec: spec.clone(),
519        commit_hash: get_git_commit(),
520        branch: get_git_branch(),
521        dirty: is_git_dirty(),
522        build_time,
523        build_time_unix: now.as_secs(),
524        target: target.to_string(),
525        profile: profile.to_string(),
526        mobench_version: env!("CARGO_PKG_VERSION").to_string(),
527        rust_version: get_rust_version(),
528        host_os: env::consts::OS.to_string(),
529    }
530}
531
532/// Convert days since epoch to (year, month, day).
533/// Simplified Gregorian calendar calculation.
534fn days_to_ymd(days: u64) -> (i32, u32, u32) {
535    let mut remaining_days = days as i64;
536    let mut year = 1970i32;
537
538    // Advance years
539    loop {
540        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
541        if remaining_days < days_in_year {
542            break;
543        }
544        remaining_days -= days_in_year;
545        year += 1;
546    }
547
548    // Days in each month (non-leap year)
549    let days_in_months: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
550
551    let mut month = 1u32;
552    for (i, &days_in_month) in days_in_months.iter().enumerate() {
553        let mut dim = days_in_month;
554        if i == 1 && is_leap_year(year) {
555            dim = 29;
556        }
557        if remaining_days < dim {
558            break;
559        }
560        remaining_days -= dim;
561        month += 1;
562    }
563
564    (year, month, remaining_days as u32 + 1)
565}
566
567fn is_leap_year(year: i32) -> bool {
568    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
569}
570
571/// Embeds build metadata (bench_meta.json) alongside bench_spec.json in mobile app bundles.
572///
573/// This function creates a `bench_meta.json` file that contains:
574/// - The benchmark specification
575/// - Git commit hash and branch (if available)
576/// - Build timestamp
577/// - Target platform and profile
578/// - mobench and Rust versions
579///
580/// # Arguments
581/// * `output_dir` - The mobench output directory (e.g., `target/mobench`)
582/// * `spec` - The benchmark specification
583/// * `target` - Target platform ("android" or "ios")
584/// * `profile` - Build profile ("debug" or "release")
585pub fn embed_bench_meta(
586    output_dir: &Path,
587    spec: &EmbeddedBenchSpec,
588    target: &str,
589    profile: &str,
590) -> Result<(), BenchError> {
591    let meta = create_bench_meta(spec, target, profile);
592    let meta_json = serde_json::to_string_pretty(&meta).map_err(|e| {
593        BenchError::Build(format!("Failed to serialize bench meta: {}", e))
594    })?;
595
596    // Android: Write to assets directory
597    let android_assets_dir = output_dir.join("android/app/src/main/assets");
598    if output_dir.join("android").exists() {
599        std::fs::create_dir_all(&android_assets_dir).map_err(|e| {
600            BenchError::Build(format!(
601                "Failed to create Android assets directory at {}: {}",
602                android_assets_dir.display(),
603                e
604            ))
605        })?;
606        let android_meta_path = android_assets_dir.join("bench_meta.json");
607        std::fs::write(&android_meta_path, &meta_json).map_err(|e| {
608            BenchError::Build(format!(
609                "Failed to write Android bench meta to {}: {}",
610                android_meta_path.display(),
611                e
612            ))
613        })?;
614    }
615
616    // iOS: Write to Resources directory in the Xcode project
617    let ios_resources_dir = output_dir.join("ios/BenchRunner/BenchRunner/Resources");
618    if output_dir.join("ios/BenchRunner").exists() {
619        std::fs::create_dir_all(&ios_resources_dir).map_err(|e| {
620            BenchError::Build(format!(
621                "Failed to create iOS Resources directory at {}: {}",
622                ios_resources_dir.display(),
623                e
624            ))
625        })?;
626        let ios_meta_path = ios_resources_dir.join("bench_meta.json");
627        std::fs::write(&ios_meta_path, &meta_json).map_err(|e| {
628            BenchError::Build(format!(
629                "Failed to write iOS bench meta to {}: {}",
630                ios_meta_path.display(),
631                e
632            ))
633        })?;
634    }
635
636    Ok(())
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    #[test]
644    fn test_get_cargo_target_dir_fallback() {
645        // For a non-existent directory, should fall back gracefully
646        let result = get_cargo_target_dir(Path::new("/nonexistent/path"));
647        // Should either error or return fallback path
648        assert!(result.is_ok() || result.is_err());
649    }
650
651    #[test]
652    fn test_host_lib_path_not_found() {
653        let result = host_lib_path(Path::new("/tmp"), "nonexistent-crate");
654        assert!(result.is_err());
655        let err = result.unwrap_err();
656        let msg = format!("{}", err);
657        assert!(msg.contains("Host library for UniFFI not found"));
658        assert!(msg.contains("cargo build"));
659    }
660
661    #[test]
662    fn test_run_command_not_found() {
663        let cmd = Command::new("nonexistent-command-12345");
664        let result = run_command(cmd, "test command");
665        assert!(result.is_err());
666        let err = result.unwrap_err();
667        let msg = format!("{}", err);
668        assert!(msg.contains("Failed to start"));
669    }
670
671    #[test]
672    fn test_read_package_name_standard() {
673        let temp_dir = std::env::temp_dir().join("mobench-test-read-package");
674        let _ = std::fs::remove_dir_all(&temp_dir);
675        std::fs::create_dir_all(&temp_dir).unwrap();
676
677        let cargo_toml = temp_dir.join("Cargo.toml");
678        std::fs::write(
679            &cargo_toml,
680            r#"[package]
681name = "my-awesome-crate"
682version = "0.1.0"
683edition = "2021"
684
685[dependencies]
686"#,
687        )
688        .unwrap();
689
690        let result = read_package_name(&cargo_toml);
691        assert_eq!(result, Some("my-awesome-crate".to_string()));
692
693        std::fs::remove_dir_all(&temp_dir).unwrap();
694    }
695
696    #[test]
697    fn test_read_package_name_with_single_quotes() {
698        let temp_dir = std::env::temp_dir().join("mobench-test-read-package-sq");
699        let _ = std::fs::remove_dir_all(&temp_dir);
700        std::fs::create_dir_all(&temp_dir).unwrap();
701
702        let cargo_toml = temp_dir.join("Cargo.toml");
703        std::fs::write(
704            &cargo_toml,
705            r#"[package]
706name = 'single-quoted-crate'
707version = "0.1.0"
708"#,
709        )
710        .unwrap();
711
712        let result = read_package_name(&cargo_toml);
713        assert_eq!(result, Some("single-quoted-crate".to_string()));
714
715        std::fs::remove_dir_all(&temp_dir).unwrap();
716    }
717
718    #[test]
719    fn test_read_package_name_not_found() {
720        let result = read_package_name(Path::new("/nonexistent/Cargo.toml"));
721        assert_eq!(result, None);
722    }
723
724    #[test]
725    fn test_read_package_name_no_package_section() {
726        let temp_dir = std::env::temp_dir().join("mobench-test-read-package-no-pkg");
727        let _ = std::fs::remove_dir_all(&temp_dir);
728        std::fs::create_dir_all(&temp_dir).unwrap();
729
730        let cargo_toml = temp_dir.join("Cargo.toml");
731        std::fs::write(
732            &cargo_toml,
733            r#"[workspace]
734members = ["crates/*"]
735"#,
736        )
737        .unwrap();
738
739        let result = read_package_name(&cargo_toml);
740        assert_eq!(result, None);
741
742        std::fs::remove_dir_all(&temp_dir).unwrap();
743    }
744
745    #[test]
746    fn test_create_bench_meta() {
747        let spec = EmbeddedBenchSpec {
748            function: "test_crate::my_benchmark".to_string(),
749            iterations: 100,
750            warmup: 10,
751        };
752
753        let meta = create_bench_meta(&spec, "android", "release");
754
755        assert_eq!(meta.spec.function, "test_crate::my_benchmark");
756        assert_eq!(meta.spec.iterations, 100);
757        assert_eq!(meta.spec.warmup, 10);
758        assert_eq!(meta.target, "android");
759        assert_eq!(meta.profile, "release");
760        assert!(!meta.mobench_version.is_empty());
761        assert!(!meta.host_os.is_empty());
762        assert!(!meta.build_time.is_empty());
763        assert!(meta.build_time_unix > 0);
764        // Build time should be in RFC3339 format (roughly YYYY-MM-DDTHH:MM:SSZ)
765        assert!(meta.build_time.contains('T'));
766        assert!(meta.build_time.ends_with('Z'));
767    }
768
769    #[test]
770    fn test_days_to_ymd_epoch() {
771        // Day 0 should be January 1, 1970
772        let (year, month, day) = days_to_ymd(0);
773        assert_eq!(year, 1970);
774        assert_eq!(month, 1);
775        assert_eq!(day, 1);
776    }
777
778    #[test]
779    fn test_days_to_ymd_known_date() {
780        // January 21, 2026 is approximately 20,474 days since epoch
781        // (2026 - 1970 = 56 years, with leap years)
782        // Let's test a simpler case: 365 days = January 1, 1971
783        let (year, month, day) = days_to_ymd(365);
784        assert_eq!(year, 1971);
785        assert_eq!(month, 1);
786        assert_eq!(day, 1);
787    }
788
789    #[test]
790    fn test_is_leap_year() {
791        assert!(!is_leap_year(1970)); // Not divisible by 4
792        assert!(is_leap_year(2000));  // Divisible by 400
793        assert!(!is_leap_year(1900)); // Divisible by 100 but not 400
794        assert!(is_leap_year(2024));  // Divisible by 4, not by 100
795    }
796
797    #[test]
798    fn test_bench_meta_serialization() {
799        let spec = EmbeddedBenchSpec {
800            function: "my_func".to_string(),
801            iterations: 50,
802            warmup: 5,
803        };
804
805        let meta = create_bench_meta(&spec, "ios", "debug");
806        let json = serde_json::to_string(&meta).expect("serialization should work");
807
808        // Verify it contains expected fields
809        assert!(json.contains("my_func"));
810        assert!(json.contains("ios"));
811        assert!(json.contains("debug"));
812        assert!(json.contains("build_time"));
813        assert!(json.contains("mobench_version"));
814    }
815}