Skip to main content

mobench_sdk/
codegen.rs

1//! Code generation and template management
2//!
3//! This module provides functionality for generating mobile app projects from
4//! embedded templates. It handles template parameterization and file generation.
5
6use crate::types::{BenchError, InitConfig, Target};
7use std::fs;
8use std::io::{BufRead, BufReader};
9use std::path::{Path, PathBuf};
10
11use include_dir::{Dir, DirEntry, include_dir};
12
13const ANDROID_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/android");
14const IOS_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/ios");
15const NATIVE_ANDROID_MAIN_ACTIVITY_TEMPLATE: &str =
16    include_str!("native_templates/android/MainActivity.kt.template");
17const NATIVE_IOS_BENCH_RUNNER_FFI_TEMPLATE: &str =
18    include_str!("native_templates/ios/BenchRunnerFFI.swift.template");
19const BOLTFFI_ANDROID_MAIN_ACTIVITY_TEMPLATE: &str =
20    include_str!("boltffi_templates/android/MainActivity.kt.template");
21const BOLTFFI_IOS_BENCH_RUNNER_FFI_TEMPLATE: &str =
22    include_str!("boltffi_templates/ios/BenchRunnerFFI.swift.template");
23pub const DEFAULT_IOS_BENCHMARK_TIMEOUT_SECS: u64 = 300;
24const DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS: u64 = 1800;
25const DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS: u64 = 10;
26
27pub const DEFAULT_IOS_DEPLOYMENT_TARGET: &str = "15.0";
28pub const SWIFTUI_RUNNER_MIN_IOS: &str = "15.0";
29
30/// Supported generated iOS application runner templates.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum IosRunner {
33    /// Current SwiftUI runner. This is the default for iOS 15+.
34    Swiftui,
35    /// UIKit-based runner for legacy deployment targets.
36    UikitLegacy,
37}
38
39impl IosRunner {
40    pub fn parse(value: &str) -> Result<Self, BenchError> {
41        match value.trim().to_ascii_lowercase().as_str() {
42            "swiftui" => Ok(Self::Swiftui),
43            "uikit-legacy" | "uikit_legacy" => Ok(Self::UikitLegacy),
44            other => Err(BenchError::Build(format!(
45                "Unsupported iOS runner `{other}`. Supported values: swiftui, uikit-legacy"
46            ))),
47        }
48    }
49
50    pub fn as_str(self) -> &'static str {
51        match self {
52            Self::Swiftui => "swiftui",
53            Self::UikitLegacy => "uikit-legacy",
54        }
55    }
56}
57
58/// Parsed iOS deployment target used for explicit compatibility decisions.
59#[derive(Debug, Clone, Eq)]
60pub struct IosDeploymentTarget {
61    raw: String,
62    major: u16,
63    minor: u16,
64}
65
66impl PartialEq for IosDeploymentTarget {
67    fn eq(&self, other: &Self) -> bool {
68        (self.major, self.minor) == (other.major, other.minor)
69    }
70}
71
72impl Ord for IosDeploymentTarget {
73    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
74        (self.major, self.minor).cmp(&(other.major, other.minor))
75    }
76}
77
78impl PartialOrd for IosDeploymentTarget {
79    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
80        Some(self.cmp(other))
81    }
82}
83
84impl IosDeploymentTarget {
85    pub fn parse(raw: &str) -> Result<Self, BenchError> {
86        let raw = raw.trim();
87        if raw.is_empty() {
88            return Err(BenchError::Build(
89                "iOS deployment target must not be empty".to_string(),
90            ));
91        }
92
93        let mut parts = raw.split('.');
94        let major_raw = parts.next().unwrap_or_default();
95        let minor_raw = parts.next().unwrap_or("0");
96        if parts.next().is_some() {
97            return Err(BenchError::Build(format!(
98                "Invalid iOS deployment target `{raw}`. Expected VERSION like 15.0"
99            )));
100        }
101
102        Ok(Self {
103            raw: raw.to_string(),
104            major: parse_ios_version_part(raw, major_raw, "major")?,
105            minor: parse_ios_version_part(raw, minor_raw, "minor")?,
106        })
107    }
108
109    pub fn default_target() -> Self {
110        Self::parse(DEFAULT_IOS_DEPLOYMENT_TARGET)
111            .expect("default iOS deployment target should be valid")
112    }
113}
114
115fn parse_ios_version_part(raw: &str, part: &str, label: &str) -> Result<u16, BenchError> {
116    if part.is_empty() || !part.chars().all(|ch| ch.is_ascii_digit()) {
117        return Err(BenchError::Build(format!(
118            "Invalid iOS deployment target `{raw}`: {label} version component must be numeric"
119        )));
120    }
121
122    part.parse::<u16>().map_err(|err| {
123        BenchError::Build(format!(
124            "Invalid iOS deployment target `{raw}`: failed to parse {label} component: {err}"
125        ))
126    })
127}
128
129impl std::fmt::Display for IosDeploymentTarget {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.write_str(&self.raw)
132    }
133}
134
135/// Fully resolved iOS project generation options.
136#[derive(Debug, Clone)]
137pub struct IosProjectOptions {
138    pub deployment_target: IosDeploymentTarget,
139    pub runner: IosRunner,
140    pub ios_benchmark_timeout_secs: u64,
141}
142
143impl Default for IosProjectOptions {
144    fn default() -> Self {
145        Self {
146            deployment_target: IosDeploymentTarget::default_target(),
147            runner: IosRunner::Swiftui,
148            ios_benchmark_timeout_secs: DEFAULT_IOS_BENCHMARK_TIMEOUT_SECS,
149        }
150    }
151}
152
153pub fn resolve_ios_runner(
154    deployment_target: &IosDeploymentTarget,
155    requested_runner: Option<IosRunner>,
156) -> Result<IosRunner, BenchError> {
157    let swiftui_floor = IosDeploymentTarget::parse(SWIFTUI_RUNNER_MIN_IOS)?;
158    match requested_runner {
159        Some(IosRunner::Swiftui) if deployment_target < &swiftui_floor => {
160            Err(BenchError::Build(format!(
161                "iOS runner `swiftui` requires deployment target {SWIFTUI_RUNNER_MIN_IOS}+; \
162                 requested deployment target is {deployment_target}. Use `uikit-legacy` for older iOS targets."
163            )))
164        }
165        Some(runner) => Ok(runner),
166        None if deployment_target < &swiftui_floor => Ok(IosRunner::UikitLegacy),
167        None => Ok(IosRunner::Swiftui),
168    }
169}
170
171/// Template variable that can be replaced in template files
172#[derive(Debug, Clone)]
173pub struct TemplateVar {
174    pub name: &'static str,
175    pub value: String,
176}
177
178/// Generates a new mobile benchmark project from templates
179///
180/// Creates the necessary directory structure and files for benchmarking on
181/// mobile platforms. This includes:
182/// - A `bench-mobile/` crate for FFI bindings
183/// - Platform-specific app projects (Android and/or iOS)
184/// - Configuration files
185///
186/// # Arguments
187///
188/// * `config` - Configuration for project initialization
189///
190/// # Returns
191///
192/// * `Ok(PathBuf)` - Path to the generated project root
193/// * `Err(BenchError)` - If generation fails
194pub fn generate_project(config: &InitConfig) -> Result<PathBuf, BenchError> {
195    let output_dir = &config.output_dir;
196    let project_slug = sanitize_package_name(&config.project_name);
197    let project_pascal = to_pascal_case(&project_slug);
198    // Use sanitized bundle ID component (alphanumeric only) to avoid iOS validation issues
199    let bundle_id_component = sanitize_bundle_id_component(&project_slug);
200    let bundle_prefix = format!("dev.world.{}", bundle_id_component);
201
202    // Create base directories
203    fs::create_dir_all(output_dir)?;
204
205    // Generate bench-mobile FFI wrapper crate
206    generate_bench_mobile_crate(output_dir, &project_slug)?;
207
208    // For full project generation (init), use "example_fibonacci" as the default
209    // since the generated example benchmarks include this function
210    let default_function = "example_fibonacci";
211
212    // Generate platform-specific projects
213    match config.target {
214        Target::Android => {
215            generate_android_project(output_dir, &project_slug, default_function)?;
216        }
217        Target::Ios => {
218            generate_ios_project(
219                output_dir,
220                &project_slug,
221                &project_pascal,
222                &bundle_prefix,
223                default_function,
224            )?;
225        }
226        Target::Both => {
227            generate_android_project(output_dir, &project_slug, default_function)?;
228            generate_ios_project(
229                output_dir,
230                &project_slug,
231                &project_pascal,
232                &bundle_prefix,
233                default_function,
234            )?;
235        }
236    }
237
238    // Generate config file
239    generate_config_file(output_dir, config)?;
240
241    // Generate examples if requested
242    if config.generate_examples {
243        generate_example_benchmarks(output_dir)?;
244    }
245
246    Ok(output_dir.clone())
247}
248
249/// Generates the bench-mobile FFI wrapper crate
250fn generate_bench_mobile_crate(output_dir: &Path, project_name: &str) -> Result<(), BenchError> {
251    let crate_dir = output_dir.join("bench-mobile");
252    fs::create_dir_all(crate_dir.join("src"))?;
253
254    let crate_name = format!("{}-bench-mobile", project_name);
255
256    // Generate Cargo.toml
257    // Note: We configure rustls to use 'ring' instead of 'aws-lc-rs' (default in rustls 0.23+)
258    // because aws-lc-rs doesn't compile for Android NDK targets.
259    let cargo_toml = format!(
260        r#"[package]
261name = "{}"
262version = "0.1.0"
263edition = "2021"
264
265[lib]
266crate-type = ["cdylib", "staticlib", "rlib"]
267
268[dependencies]
269mobench-sdk = {{ path = "..", default-features = false, features = ["registry"] }}
270uniffi = "0.28"
271{} = {{ path = ".." }}
272
273[features]
274default = []
275
276[build-dependencies]
277uniffi = {{ version = "0.28", features = ["build"] }}
278
279# Binary for generating UniFFI bindings (used by mobench build)
280[[bin]]
281name = "uniffi-bindgen"
282path = "src/bin/uniffi-bindgen.rs"
283
284# IMPORTANT: If your project uses rustls (directly or transitively), you must configure
285# it to use the 'ring' crypto backend instead of 'aws-lc-rs' (the default in rustls 0.23+).
286# aws-lc-rs doesn't compile for Android NDK targets due to C compilation issues.
287#
288# Add this to your root Cargo.toml:
289# [workspace.dependencies]
290# rustls = {{ version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }}
291#
292# Then in each crate that uses rustls:
293# [dependencies]
294# rustls = {{ workspace = true }}
295"#,
296        crate_name, project_name
297    );
298
299    fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
300
301    // Generate src/lib.rs
302    let lib_rs_template = r#"//! Mobile FFI bindings for benchmarks
303//!
304//! This crate provides the FFI boundary between Rust benchmarks and mobile
305//! platforms (Android/iOS). It uses UniFFI to generate type-safe bindings.
306
307use uniffi;
308
309// Ensure the user crate is linked so benchmark registrations are pulled in.
310extern crate {{USER_CRATE}} as _bench_user_crate;
311
312// Re-export mobench-sdk types with UniFFI annotations
313#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
314pub struct BenchSpec {
315    pub name: String,
316    pub iterations: u32,
317    pub warmup: u32,
318}
319
320#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
321pub struct BenchSample {
322    pub duration_ns: u64,
323    pub cpu_time_ms: Option<u64>,
324    pub peak_memory_kb: Option<u64>,
325    pub process_peak_memory_kb: Option<u64>,
326}
327
328#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
329pub struct SemanticPhase {
330    pub name: String,
331    pub duration_ns: u64,
332}
333
334#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
335pub struct HarnessTimelineSpan {
336    pub phase: String,
337    pub start_offset_ns: u64,
338    pub end_offset_ns: u64,
339    pub iteration: Option<u32>,
340}
341
342#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
343pub struct BenchReport {
344    pub spec: BenchSpec,
345    pub samples: Vec<BenchSample>,
346    pub phases: Vec<SemanticPhase>,
347    pub timeline: Vec<HarnessTimelineSpan>,
348}
349
350#[derive(Debug, thiserror::Error, uniffi::Error)]
351#[uniffi(flat_error)]
352pub enum BenchError {
353    #[error("iterations must be greater than zero")]
354    InvalidIterations,
355
356    #[error("unknown benchmark function: {name}")]
357    UnknownFunction { name: String },
358
359    #[error("benchmark execution failed: {reason}")]
360    ExecutionFailed { reason: String },
361}
362
363// Convert from mobench-sdk types
364impl From<mobench_sdk::BenchSpec> for BenchSpec {
365    fn from(spec: mobench_sdk::BenchSpec) -> Self {
366        Self {
367            name: spec.name,
368            iterations: spec.iterations,
369            warmup: spec.warmup,
370        }
371    }
372}
373
374impl From<BenchSpec> for mobench_sdk::BenchSpec {
375    fn from(spec: BenchSpec) -> Self {
376        Self {
377            name: spec.name,
378            iterations: spec.iterations,
379            warmup: spec.warmup,
380        }
381    }
382}
383
384impl From<mobench_sdk::BenchSample> for BenchSample {
385    fn from(sample: mobench_sdk::BenchSample) -> Self {
386        Self {
387            duration_ns: sample.duration_ns,
388            cpu_time_ms: sample.cpu_time_ms,
389            peak_memory_kb: sample.peak_memory_kb,
390            process_peak_memory_kb: sample.process_peak_memory_kb,
391        }
392    }
393}
394
395impl From<mobench_sdk::SemanticPhase> for SemanticPhase {
396    fn from(phase: mobench_sdk::SemanticPhase) -> Self {
397        Self {
398            name: phase.name,
399            duration_ns: phase.duration_ns,
400        }
401    }
402}
403
404impl From<mobench_sdk::HarnessTimelineSpan> for HarnessTimelineSpan {
405    fn from(span: mobench_sdk::HarnessTimelineSpan) -> Self {
406        Self {
407            phase: span.phase,
408            start_offset_ns: span.start_offset_ns,
409            end_offset_ns: span.end_offset_ns,
410            iteration: span.iteration,
411        }
412    }
413}
414
415impl From<mobench_sdk::RunnerReport> for BenchReport {
416    fn from(report: mobench_sdk::RunnerReport) -> Self {
417        Self {
418            spec: report.spec.into(),
419            samples: report.samples.into_iter().map(Into::into).collect(),
420            phases: report.phases.into_iter().map(Into::into).collect(),
421            timeline: report.timeline.into_iter().map(Into::into).collect(),
422        }
423    }
424}
425
426impl From<mobench_sdk::BenchError> for BenchError {
427    fn from(err: mobench_sdk::BenchError) -> Self {
428        match err {
429            mobench_sdk::BenchError::Runner(runner_err) => {
430                BenchError::ExecutionFailed {
431                    reason: runner_err.to_string(),
432                }
433            }
434            mobench_sdk::BenchError::UnknownFunction(name, _available) => {
435                BenchError::UnknownFunction { name }
436            }
437            _ => BenchError::ExecutionFailed {
438                reason: err.to_string(),
439            },
440        }
441    }
442}
443
444/// Runs a benchmark by name with the given specification
445///
446/// This is the main FFI entry point called from mobile platforms.
447#[uniffi::export]
448pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
449    let sdk_spec: mobench_sdk::BenchSpec = spec.into();
450    let report = mobench_sdk::run_benchmark(sdk_spec)?;
451    Ok(report.into())
452}
453
454// Generate UniFFI scaffolding
455uniffi::setup_scaffolding!();
456"#;
457
458    let lib_rs = render_template(
459        lib_rs_template,
460        &[TemplateVar {
461            name: "USER_CRATE",
462            value: project_name.replace('-', "_"),
463        }],
464    );
465    fs::write(crate_dir.join("src/lib.rs"), lib_rs)?;
466
467    // Generate build.rs
468    let build_rs = r#"fn main() {
469    uniffi::generate_scaffolding("src/lib.rs").unwrap();
470}
471"#;
472
473    fs::write(crate_dir.join("build.rs"), build_rs)?;
474
475    // Generate uniffi-bindgen binary (used by mobench build)
476    let bin_dir = crate_dir.join("src/bin");
477    fs::create_dir_all(&bin_dir)?;
478    let uniffi_bindgen_rs = r#"fn main() {
479    uniffi::uniffi_bindgen_main()
480}
481"#;
482    fs::write(bin_dir.join("uniffi-bindgen.rs"), uniffi_bindgen_rs)?;
483
484    Ok(())
485}
486
487/// Generates Android project structure from templates
488///
489/// This function can be called standalone to generate just the Android
490/// project scaffolding, useful for auto-generation during build.
491///
492/// # Arguments
493///
494/// * `output_dir` - Directory to write the `android/` project into
495/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
496/// * `default_function` - Default benchmark function to use (e.g., "bench_mobile::my_benchmark")
497pub fn generate_android_project(
498    output_dir: &Path,
499    project_slug: &str,
500    default_function: &str,
501) -> Result<(), BenchError> {
502    generate_android_project_with_backend(
503        output_dir,
504        project_slug,
505        default_function,
506        crate::FfiBackend::Uniffi,
507    )
508}
509
510/// Generates Android project structure from templates for a specific FFI backend.
511pub fn generate_android_project_with_backend(
512    output_dir: &Path,
513    project_slug: &str,
514    default_function: &str,
515    ffi_backend: crate::FfiBackend,
516) -> Result<(), BenchError> {
517    let android_benchmark_timeout_secs = resolve_positive_u64_env(
518        "MOBENCH_ANDROID_BENCHMARK_TIMEOUT_SECS",
519        DEFAULT_ANDROID_BENCHMARK_TIMEOUT_SECS,
520    );
521    let android_heartbeat_interval_secs = resolve_positive_u64_env(
522        "MOBENCH_ANDROID_HEARTBEAT_INTERVAL_SECS",
523        DEFAULT_ANDROID_HEARTBEAT_INTERVAL_SECS,
524    );
525    generate_android_project_with_options(
526        output_dir,
527        project_slug,
528        default_function,
529        ffi_backend,
530        android_benchmark_timeout_secs,
531        android_heartbeat_interval_secs,
532    )
533}
534
535fn generate_android_project_with_options(
536    output_dir: &Path,
537    project_slug: &str,
538    default_function: &str,
539    ffi_backend: crate::FfiBackend,
540    android_benchmark_timeout_secs: u64,
541    android_heartbeat_interval_secs: u64,
542) -> Result<(), BenchError> {
543    let target_dir = output_dir.join("android");
544    reset_generated_project_dir(&target_dir)?;
545    let library_name = project_slug.replace('-', "_");
546    let project_pascal = to_pascal_case(project_slug);
547    // Use sanitized bundle ID component (alphanumeric only) for consistency with iOS
548    // This ensures both platforms use the same naming convention: "benchmobile" not "bench-mobile"
549    let package_id_component = sanitize_bundle_id_component(project_slug);
550    let package_name = format!("dev.world.{}", package_id_component);
551    let boltffi_kotlin_package = format!("com.mobench.{}", package_id_component);
552    let vars = vec![
553        TemplateVar {
554            name: "PROJECT_NAME",
555            value: project_slug.to_string(),
556        },
557        TemplateVar {
558            name: "PROJECT_NAME_PASCAL",
559            value: project_pascal.clone(),
560        },
561        TemplateVar {
562            name: "APP_NAME",
563            value: format!("{} Benchmark", project_pascal),
564        },
565        TemplateVar {
566            name: "PACKAGE_NAME",
567            value: package_name.clone(),
568        },
569        TemplateVar {
570            name: "BOLTFFI_KOTLIN_PACKAGE",
571            value: boltffi_kotlin_package.clone(),
572        },
573        TemplateVar {
574            name: "UNIFFI_NAMESPACE",
575            value: library_name.clone(),
576        },
577        TemplateVar {
578            name: "LIBRARY_NAME",
579            value: library_name,
580        },
581        TemplateVar {
582            name: "DEFAULT_FUNCTION",
583            value: default_function.to_string(),
584        },
585        TemplateVar {
586            name: "ANDROID_BENCHMARK_TIMEOUT_SECS",
587            value: android_benchmark_timeout_secs.to_string(),
588        },
589        TemplateVar {
590            name: "ANDROID_HEARTBEAT_INTERVAL_SECS",
591            value: android_heartbeat_interval_secs.to_string(),
592        },
593    ];
594    render_dir(&ANDROID_TEMPLATES, &target_dir, &vars)?;
595
596    // Move Kotlin files to the correct package directory structure
597    // The package "dev.world.{project_slug}" maps to directory "dev/world/{project_slug}/"
598    move_kotlin_files_to_package_dir(&target_dir, &package_name)?;
599    match ffi_backend {
600        crate::FfiBackend::Uniffi => {}
601        crate::FfiBackend::NativeCAbi => {
602            write_native_android_main_activity(&target_dir, &package_name, &vars)?;
603        }
604        crate::FfiBackend::BoltFfi => {
605            write_boltffi_android_main_activity(&target_dir, &package_name, &vars)?;
606            write_boltffi_config(
607                output_dir,
608                project_slug,
609                "BenchRunner",
610                &boltffi_kotlin_package,
611            )?;
612        }
613    }
614
615    Ok(())
616}
617
618fn write_native_android_main_activity(
619    target_dir: &Path,
620    package_name: &str,
621    vars: &[TemplateVar],
622) -> Result<(), BenchError> {
623    let relative_package = package_name.replace('.', "/");
624    let path = target_dir
625        .join("app/src/main/java")
626        .join(relative_package)
627        .join("MainActivity.kt");
628    let rendered = render_template(NATIVE_ANDROID_MAIN_ACTIVITY_TEMPLATE, vars);
629    validate_no_unreplaced_placeholders(&rendered, Path::new("MainActivity.kt"))?;
630    fs::write(&path, rendered).map_err(BenchError::Io)
631}
632
633fn write_boltffi_android_main_activity(
634    target_dir: &Path,
635    package_name: &str,
636    vars: &[TemplateVar],
637) -> Result<(), BenchError> {
638    let relative_package = package_name.replace('.', "/");
639    let path = target_dir
640        .join("app/src/main/java")
641        .join(relative_package)
642        .join("MainActivity.kt");
643    let rendered = render_template(BOLTFFI_ANDROID_MAIN_ACTIVITY_TEMPLATE, vars);
644    validate_no_unreplaced_placeholders(&rendered, Path::new("MainActivity.kt"))?;
645    fs::write(&path, rendered).map_err(BenchError::Io)
646}
647
648fn collect_preserved_files(
649    root: &Path,
650    current: &Path,
651    preserved: &mut Vec<(PathBuf, Vec<u8>)>,
652) -> Result<(), BenchError> {
653    let mut entries = fs::read_dir(current)?
654        .collect::<Result<Vec<_>, _>>()
655        .map_err(BenchError::Io)?;
656    entries.sort_by_key(|entry| entry.path());
657
658    for entry in entries {
659        let path = entry.path();
660        if path.is_dir() {
661            collect_preserved_files(root, &path, preserved)?;
662            continue;
663        }
664
665        let relative = path.strip_prefix(root).map_err(|e| {
666            BenchError::Build(format!(
667                "Failed to preserve generated resource {:?}: {}",
668                path, e
669            ))
670        })?;
671        preserved.push((relative.to_path_buf(), fs::read(&path)?));
672    }
673
674    Ok(())
675}
676
677fn collect_preserved_ios_resources(
678    target_dir: &Path,
679) -> Result<Vec<(PathBuf, Vec<u8>)>, BenchError> {
680    let resources_dir = target_dir.join("BenchRunner/BenchRunner/Resources");
681    let mut preserved = Vec::new();
682
683    if resources_dir.exists() {
684        collect_preserved_files(&resources_dir, &resources_dir, &mut preserved)?;
685    }
686
687    Ok(preserved)
688}
689
690fn restore_preserved_ios_resources(
691    target_dir: &Path,
692    preserved_resources: &[(PathBuf, Vec<u8>)],
693) -> Result<(), BenchError> {
694    if preserved_resources.is_empty() {
695        return Ok(());
696    }
697
698    let resources_dir = target_dir.join("BenchRunner/BenchRunner/Resources");
699    for (relative, contents) in preserved_resources {
700        let resource_path = resources_dir.join(relative);
701        if let Some(parent) = resource_path.parent() {
702            fs::create_dir_all(parent)?;
703        }
704        fs::write(resource_path, contents)?;
705    }
706
707    Ok(())
708}
709
710fn reset_generated_project_dir(target_dir: &Path) -> Result<(), BenchError> {
711    if target_dir.exists() {
712        fs::remove_dir_all(target_dir).map_err(|e| {
713            BenchError::Build(format!(
714                "Failed to clear existing generated project at {:?}: {}",
715                target_dir, e
716            ))
717        })?;
718    }
719    Ok(())
720}
721
722/// Moves Kotlin source files to the correct package directory structure
723///
724/// Android requires source files to be in directories matching their package declaration.
725/// For example, a file with `package dev.world.my_project` must be in
726/// `app/src/main/java/dev/world/my_project/`.
727///
728/// This function moves:
729/// - MainActivity.kt from `app/src/main/java/` to `app/src/main/java/{package_path}/`
730/// - MainActivityTest.kt from `app/src/androidTest/java/` to `app/src/androidTest/java/{package_path}/`
731fn move_kotlin_files_to_package_dir(
732    android_dir: &Path,
733    package_name: &str,
734) -> Result<(), BenchError> {
735    // Convert package name to directory path (e.g., "dev.world.my_project" -> "dev/world/my_project")
736    let package_path = package_name.replace('.', "/");
737
738    // Move main source files
739    let main_java_dir = android_dir.join("app/src/main/java");
740    let main_package_dir = main_java_dir.join(&package_path);
741    move_kotlin_file(&main_java_dir, &main_package_dir, "MainActivity.kt")?;
742
743    // Move test source files
744    let test_java_dir = android_dir.join("app/src/androidTest/java");
745    let test_package_dir = test_java_dir.join(&package_path);
746    move_kotlin_file(&test_java_dir, &test_package_dir, "MainActivityTest.kt")?;
747
748    Ok(())
749}
750
751/// Moves a single Kotlin file from source directory to package directory
752fn move_kotlin_file(src_dir: &Path, dest_dir: &Path, filename: &str) -> Result<(), BenchError> {
753    let src_file = src_dir.join(filename);
754    if !src_file.exists() {
755        // File doesn't exist in source, nothing to move
756        return Ok(());
757    }
758
759    // Create the package directory if it doesn't exist
760    fs::create_dir_all(dest_dir).map_err(|e| {
761        BenchError::Build(format!(
762            "Failed to create package directory {:?}: {}",
763            dest_dir, e
764        ))
765    })?;
766
767    let dest_file = dest_dir.join(filename);
768
769    // Move the file (copy + delete for cross-filesystem compatibility)
770    fs::copy(&src_file, &dest_file).map_err(|e| {
771        BenchError::Build(format!(
772            "Failed to copy {} to {:?}: {}",
773            filename, dest_file, e
774        ))
775    })?;
776
777    fs::remove_file(&src_file).map_err(|e| {
778        BenchError::Build(format!(
779            "Failed to remove original file {:?}: {}",
780            src_file, e
781        ))
782    })?;
783
784    Ok(())
785}
786
787/// Generates iOS project structure from templates
788///
789/// This function can be called standalone to generate just the iOS
790/// project scaffolding, useful for auto-generation during build.
791///
792/// # Arguments
793///
794/// * `output_dir` - Directory to write the `ios/` project into
795/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
796/// * `project_pascal` - PascalCase version of project name (e.g., "BenchMobile")
797/// * `bundle_prefix` - iOS bundle ID prefix (e.g., "dev.world.bench")
798/// * `default_function` - Default benchmark function to use (e.g., "bench_mobile::my_benchmark")
799pub fn generate_ios_project(
800    output_dir: &Path,
801    project_slug: &str,
802    project_pascal: &str,
803    bundle_prefix: &str,
804    default_function: &str,
805) -> Result<(), BenchError> {
806    generate_ios_project_with_backend_options(
807        output_dir,
808        project_slug,
809        project_pascal,
810        bundle_prefix,
811        default_function,
812        crate::FfiBackend::Uniffi,
813        IosProjectOptions {
814            ios_benchmark_timeout_secs: resolve_ios_benchmark_timeout_secs(
815                std::env::var("MOBENCH_IOS_BENCHMARK_TIMEOUT_SECS")
816                    .ok()
817                    .as_deref(),
818            ),
819            ..IosProjectOptions::default()
820        },
821    )
822}
823
824/// Generates iOS project structure from templates for a specific FFI backend.
825pub fn generate_ios_project_with_backend(
826    output_dir: &Path,
827    project_slug: &str,
828    project_pascal: &str,
829    bundle_prefix: &str,
830    default_function: &str,
831    ffi_backend: crate::FfiBackend,
832) -> Result<(), BenchError> {
833    generate_ios_project_with_backend_options(
834        output_dir,
835        project_slug,
836        project_pascal,
837        bundle_prefix,
838        default_function,
839        ffi_backend,
840        IosProjectOptions {
841            ios_benchmark_timeout_secs: resolve_ios_benchmark_timeout_secs(
842                std::env::var("MOBENCH_IOS_BENCHMARK_TIMEOUT_SECS")
843                    .ok()
844                    .as_deref(),
845            ),
846            ..IosProjectOptions::default()
847        },
848    )
849}
850
851#[cfg(test)]
852fn generate_ios_project_with_timeout(
853    output_dir: &Path,
854    project_slug: &str,
855    project_pascal: &str,
856    bundle_prefix: &str,
857    default_function: &str,
858    ios_benchmark_timeout_secs: u64,
859    ffi_backend: crate::FfiBackend,
860) -> Result<(), BenchError> {
861    generate_ios_project_with_backend_options(
862        output_dir,
863        project_slug,
864        project_pascal,
865        bundle_prefix,
866        default_function,
867        ffi_backend,
868        IosProjectOptions {
869            ios_benchmark_timeout_secs,
870            ..IosProjectOptions::default()
871        },
872    )
873}
874
875pub fn generate_ios_project_with_backend_options(
876    output_dir: &Path,
877    project_slug: &str,
878    project_pascal: &str,
879    bundle_prefix: &str,
880    default_function: &str,
881    ffi_backend: crate::FfiBackend,
882    options: IosProjectOptions,
883) -> Result<(), BenchError> {
884    let runner = resolve_ios_runner(&options.deployment_target, Some(options.runner))?;
885    let target_dir = output_dir.join("ios");
886    let preserved_resources = collect_preserved_ios_resources(&target_dir)?;
887    reset_generated_project_dir(&target_dir)?;
888    // Sanitize bundle ID components to ensure they only contain alphanumeric characters
889    // iOS bundle identifiers should not contain hyphens or underscores
890    let sanitized_bundle_prefix = {
891        let parts: Vec<&str> = bundle_prefix.split('.').collect();
892        parts
893            .iter()
894            .map(|part| sanitize_bundle_id_component(part))
895            .collect::<Vec<_>>()
896            .join(".")
897    };
898    // Use the actual app name (project_pascal, e.g., "BenchRunner") for the bundle ID suffix,
899    // not the crate name again. This prevents duplication like "dev.world.benchmobile.benchmobile"
900    // and produces the correct "dev.world.benchmobile.BenchRunner"
901    let vars = vec![
902        TemplateVar {
903            name: "DEFAULT_FUNCTION",
904            value: default_function.to_string(),
905        },
906        TemplateVar {
907            name: "PROJECT_NAME_PASCAL",
908            value: project_pascal.to_string(),
909        },
910        TemplateVar {
911            name: "BUNDLE_ID_PREFIX",
912            value: sanitized_bundle_prefix.clone(),
913        },
914        TemplateVar {
915            name: "BUNDLE_ID",
916            value: format!("{}.{}", sanitized_bundle_prefix, project_pascal),
917        },
918        TemplateVar {
919            name: "LIBRARY_NAME",
920            value: project_slug.replace('-', "_"),
921        },
922        TemplateVar {
923            name: "IOS_BENCHMARK_TIMEOUT_SECS",
924            value: options.ios_benchmark_timeout_secs.to_string(),
925        },
926        TemplateVar {
927            name: "IOS_DEPLOYMENT_TARGET",
928            value: options.deployment_target.to_string(),
929        },
930        TemplateVar {
931            name: "IOS_RUNNER",
932            value: runner.as_str().to_string(),
933        },
934        TemplateVar {
935            name: "BRIDGING_HEADER_IMPORTS",
936            value: match ffi_backend {
937                crate::FfiBackend::BoltFfi => String::new(),
938                _ => format!("#import \"{}FFI.h\"", project_slug.replace('-', "_")),
939            },
940        },
941    ];
942    render_ios_dir(&IOS_TEMPLATES, &target_dir, &vars, runner)?;
943    match ffi_backend {
944        crate::FfiBackend::Uniffi => {}
945        crate::FfiBackend::NativeCAbi => {
946            write_native_ios_bench_runner_ffi(&target_dir, project_pascal, &vars)?;
947        }
948        crate::FfiBackend::BoltFfi => {
949            write_boltffi_ios_bench_runner_ffi(&target_dir, project_pascal, &vars)?;
950            write_boltffi_config(
951                output_dir,
952                project_slug,
953                project_pascal,
954                "dev.world.benchrunner",
955            )?;
956        }
957    }
958    restore_preserved_ios_resources(&target_dir, &preserved_resources)?;
959    Ok(())
960}
961
962fn write_native_ios_bench_runner_ffi(
963    target_dir: &Path,
964    project_pascal: &str,
965    vars: &[TemplateVar],
966) -> Result<(), BenchError> {
967    let app_dir = target_dir.join("BenchRunner").join(project_pascal);
968    let path = app_dir.join("BenchRunnerFFI.swift");
969    let generated_dir = app_dir.join("Generated");
970    fs::create_dir_all(&generated_dir).map_err(BenchError::Io)?;
971    let library_name = vars
972        .iter()
973        .find(|var| var.name == "LIBRARY_NAME")
974        .map(|var| var.value.as_str())
975        .unwrap_or("bench_mobile");
976    let header_path = generated_dir.join(format!("{library_name}FFI.h"));
977    fs::write(&header_path, native_c_abi_header(library_name)).map_err(BenchError::Io)?;
978
979    let rendered = render_template(NATIVE_IOS_BENCH_RUNNER_FFI_TEMPLATE, vars);
980    validate_no_unreplaced_placeholders(&rendered, Path::new("BenchRunnerFFI.swift"))?;
981    fs::write(&path, rendered).map_err(BenchError::Io)
982}
983
984fn write_boltffi_ios_bench_runner_ffi(
985    target_dir: &Path,
986    project_pascal: &str,
987    vars: &[TemplateVar],
988) -> Result<(), BenchError> {
989    let app_dir = target_dir.join("BenchRunner").join(project_pascal);
990    let path = app_dir.join("BenchRunnerFFI.swift");
991    let generated_dir = app_dir.join("Generated").join("BoltFFIGenerated");
992    fs::create_dir_all(&generated_dir).map_err(BenchError::Io)?;
993
994    let rendered = render_template(BOLTFFI_IOS_BENCH_RUNNER_FFI_TEMPLATE, vars);
995    validate_no_unreplaced_placeholders(&rendered, Path::new("BenchRunnerFFI.swift"))?;
996    fs::write(&path, rendered).map_err(BenchError::Io)
997}
998
999pub fn write_boltffi_config(
1000    project_root: &Path,
1001    library_name: &str,
1002    swift_module_name: &str,
1003    kotlin_package: &str,
1004) -> Result<(), BenchError> {
1005    write_boltffi_config_with_options(
1006        project_root,
1007        library_name,
1008        library_name,
1009        swift_module_name,
1010        kotlin_package,
1011        &["arm64".to_string()],
1012    )
1013}
1014
1015pub fn write_boltffi_config_with_options(
1016    project_root: &Path,
1017    package_name: &str,
1018    crate_name: &str,
1019    swift_module_name: &str,
1020    kotlin_package: &str,
1021    android_architectures: &[String],
1022) -> Result<(), BenchError> {
1023    write_boltffi_config_with_paths(
1024        project_root,
1025        package_name,
1026        crate_name,
1027        swift_module_name,
1028        kotlin_package,
1029        android_architectures,
1030        &BoltFfiOutputPaths::default(),
1031    )
1032}
1033
1034pub fn write_boltffi_config_with_output_dir(
1035    project_root: &Path,
1036    package_name: &str,
1037    crate_name: &str,
1038    swift_module_name: &str,
1039    kotlin_package: &str,
1040    android_architectures: &[String],
1041    output_dir: &Path,
1042) -> Result<(), BenchError> {
1043    write_boltffi_config_with_paths(
1044        project_root,
1045        package_name,
1046        crate_name,
1047        swift_module_name,
1048        kotlin_package,
1049        android_architectures,
1050        &BoltFfiOutputPaths::for_output_dir(output_dir),
1051    )
1052}
1053
1054struct BoltFfiOutputPaths {
1055    android_kotlin: String,
1056    android_header: String,
1057    android_pack: String,
1058    apple_swift: String,
1059    apple_header: String,
1060    apple_xcframework: String,
1061}
1062
1063impl BoltFfiOutputPaths {
1064    fn default() -> Self {
1065        Self {
1066            android_kotlin: "target/mobench/android/app/src/main/java".to_string(),
1067            android_header: "target/mobench/boltffi/android/include".to_string(),
1068            android_pack: "target/mobench/android/app/src/main/jniLibs".to_string(),
1069            apple_swift: "target/mobench/ios/BenchRunner/BenchRunner/Generated/BoltFFIGenerated"
1070                .to_string(),
1071            apple_header: "target/mobench/ios/include".to_string(),
1072            apple_xcframework: "target/mobench/ios".to_string(),
1073        }
1074    }
1075
1076    fn for_output_dir(output_dir: &Path) -> Self {
1077        Self {
1078            android_kotlin: toml_path(output_dir.join("android/app/src/main/java")),
1079            android_header: toml_path(output_dir.join("boltffi/android/include")),
1080            android_pack: toml_path(output_dir.join("android/app/src/main/jniLibs")),
1081            apple_swift: toml_path(
1082                output_dir.join("ios/BenchRunner/BenchRunner/Generated/BoltFFIGenerated"),
1083            ),
1084            apple_header: toml_path(output_dir.join("ios/include")),
1085            apple_xcframework: toml_path(output_dir.join("ios")),
1086        }
1087    }
1088}
1089
1090fn toml_path(path: PathBuf) -> String {
1091    path.to_string_lossy().replace('\\', "\\\\")
1092}
1093
1094fn write_boltffi_config_with_paths(
1095    project_root: &Path,
1096    package_name: &str,
1097    crate_name: &str,
1098    swift_module_name: &str,
1099    kotlin_package: &str,
1100    android_architectures: &[String],
1101    paths: &BoltFfiOutputPaths,
1102) -> Result<(), BenchError> {
1103    let library_name = crate_name.replace('-', "_");
1104    let android_architectures = android_architectures
1105        .iter()
1106        .map(|arch| format!("\"{arch}\""))
1107        .collect::<Vec<_>>()
1108        .join(", ");
1109    let content = format!(
1110        r#"[package]
1111name = "{package_name}"
1112crate = "{crate_name}"
1113
1114[targets.android]
1115enabled = true
1116architectures = [{android_architectures}]
1117
1118[targets.android.kotlin]
1119package = "{kotlin_package}"
1120output = "{android_kotlin}"
1121library_name = "{library_name}"
1122desktop_loader = "system"
1123api_style = "top_level"
1124
1125[targets.android.header]
1126output = "{android_header}"
1127
1128[targets.android.pack]
1129output = "{android_pack}"
1130
1131[targets.apple]
1132enabled = true
1133include_macos = false
1134
1135[targets.apple.swift]
1136module_name = "{swift_module_name}"
1137output = "{apple_swift}"
1138ffi_module_name = "{library_name}FFI"
1139
1140[targets.apple.header]
1141output = "{apple_header}"
1142
1143[targets.apple.xcframework]
1144output = "{apple_xcframework}"
1145name = "{library_name}"
1146
1147[targets.apple.spm]
1148layout = "split"
1149skip_package_swift = true
1150"#,
1151        android_kotlin = paths.android_kotlin,
1152        android_header = paths.android_header,
1153        android_pack = paths.android_pack,
1154        apple_swift = paths.apple_swift,
1155        apple_header = paths.apple_header,
1156        apple_xcframework = paths.apple_xcframework,
1157    );
1158    fs::write(project_root.join("boltffi.toml"), content).map_err(BenchError::Io)
1159}
1160
1161fn native_c_abi_header(framework_name: &str) -> String {
1162    let guard = format!(
1163        "{}_MOBENCH_NATIVE_C_ABI_H",
1164        framework_name.to_ascii_uppercase()
1165    )
1166    .replace('-', "_");
1167    format!(
1168        r#"#ifndef {guard}
1169#define {guard}
1170
1171#include <stdint.h>
1172#include <stddef.h>
1173
1174#ifdef __cplusplus
1175extern "C" {{
1176#endif
1177
1178typedef struct MobenchBuf {{
1179    uint8_t *ptr;
1180    uintptr_t len;
1181    uintptr_t cap;
1182}} MobenchBuf;
1183
1184int32_t mobench_run_benchmark_json(const uint8_t *spec_ptr, uintptr_t spec_len, MobenchBuf *out);
1185void mobench_free_buf(MobenchBuf *buf);
1186const char *mobench_last_error_message(void);
1187
1188#ifdef __cplusplus
1189}}
1190#endif
1191
1192#endif
1193"#
1194    )
1195}
1196
1197fn resolve_ios_benchmark_timeout_secs(value: Option<&str>) -> u64 {
1198    value
1199        .and_then(|raw| raw.parse::<u64>().ok())
1200        .filter(|secs| *secs > 0)
1201        .unwrap_or(DEFAULT_IOS_BENCHMARK_TIMEOUT_SECS)
1202}
1203
1204fn resolve_positive_u64_env(name: &str, default: u64) -> u64 {
1205    std::env::var(name)
1206        .ok()
1207        .and_then(|raw| raw.parse::<u64>().ok())
1208        .filter(|secs| *secs > 0)
1209        .unwrap_or(default)
1210}
1211
1212/// Generates bench-config.toml configuration file
1213fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
1214    let config_target = match config.target {
1215        Target::Ios => "ios",
1216        Target::Android | Target::Both => "android",
1217    };
1218    let config_content = format!(
1219        r#"# mobench configuration
1220# This file controls how benchmarks are executed on devices.
1221
1222target = "{}"
1223function = "example_fibonacci"
1224iterations = 100
1225warmup = 10
1226device_matrix = "device-matrix.yaml"
1227device_tags = ["default"]
1228
1229[browserstack]
1230app_automate_username = "${{BROWSERSTACK_USERNAME}}"
1231app_automate_access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
1232project = "{}-benchmarks"
1233
1234[ios_xcuitest]
1235app = "target/ios/BenchRunner.ipa"
1236test_suite = "target/ios/BenchRunnerUITests.zip"
1237"#,
1238        config_target, config.project_name
1239    );
1240
1241    fs::write(output_dir.join("bench-config.toml"), config_content)?;
1242
1243    Ok(())
1244}
1245
1246/// Generates example benchmark functions
1247fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
1248    let examples_dir = output_dir.join("benches");
1249    fs::create_dir_all(&examples_dir)?;
1250
1251    let example_content = r#"//! Example benchmarks
1252//!
1253//! This file demonstrates how to write benchmarks with mobench-sdk.
1254
1255use mobench_sdk::benchmark;
1256
1257/// Simple benchmark example
1258#[benchmark]
1259fn example_fibonacci() {
1260    let result = fibonacci(30);
1261    std::hint::black_box(result);
1262}
1263
1264/// Another example with a loop
1265#[benchmark]
1266fn example_sum() {
1267    let mut sum = 0u64;
1268    for i in 0..10000 {
1269        sum = sum.wrapping_add(i);
1270    }
1271    std::hint::black_box(sum);
1272}
1273
1274// Helper function (not benchmarked)
1275fn fibonacci(n: u32) -> u64 {
1276    match n {
1277        0 => 0,
1278        1 => 1,
1279        _ => {
1280            let mut a = 0u64;
1281            let mut b = 1u64;
1282            for _ in 2..=n {
1283                let next = a.wrapping_add(b);
1284                a = b;
1285                b = next;
1286            }
1287            b
1288        }
1289    }
1290}
1291"#;
1292
1293    fs::write(examples_dir.join("example.rs"), example_content)?;
1294
1295    Ok(())
1296}
1297
1298/// File extensions that should be processed for template variable substitution
1299const TEMPLATE_EXTENSIONS: &[&str] = &[
1300    "gradle",
1301    "xml",
1302    "kt",
1303    "java",
1304    "swift",
1305    "yml",
1306    "yaml",
1307    "json",
1308    "toml",
1309    "md",
1310    "txt",
1311    "h",
1312    "m",
1313    "plist",
1314    "pbxproj",
1315    "xcscheme",
1316    "xcworkspacedata",
1317    "entitlements",
1318    "modulemap",
1319];
1320
1321fn render_dir(dir: &Dir, out_root: &Path, vars: &[TemplateVar]) -> Result<(), BenchError> {
1322    render_dir_filtered(dir, out_root, vars, &|_| false)
1323}
1324
1325fn render_ios_dir(
1326    dir: &Dir,
1327    out_root: &Path,
1328    vars: &[TemplateVar],
1329    runner: IosRunner,
1330) -> Result<(), BenchError> {
1331    render_dir_filtered(dir, out_root, vars, &|path| match runner {
1332        IosRunner::Swiftui => {
1333            path == Path::new("BenchRunner/BenchRunner/UIKitLegacyRunner.swift.template")
1334        }
1335        IosRunner::UikitLegacy => {
1336            path == Path::new("BenchRunner/BenchRunner/BenchRunnerApp.swift.template")
1337                || path == Path::new("BenchRunner/BenchRunner/ContentView.swift.template")
1338        }
1339    })
1340}
1341
1342fn render_dir_filtered(
1343    dir: &Dir,
1344    out_root: &Path,
1345    vars: &[TemplateVar],
1346    skip_file: &dyn Fn(&Path) -> bool,
1347) -> Result<(), BenchError> {
1348    for entry in dir.entries() {
1349        match entry {
1350            DirEntry::Dir(sub) => {
1351                // Skip cache directories
1352                if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
1353                    continue;
1354                }
1355                render_dir_filtered(sub, out_root, vars, skip_file)?;
1356            }
1357            DirEntry::File(file) => {
1358                if file.path().components().any(|c| c.as_os_str() == ".gradle") {
1359                    continue;
1360                }
1361                if skip_file(file.path()) {
1362                    continue;
1363                }
1364                // file.path() returns the full relative path from the embedded dir root
1365                let mut relative = file.path().to_path_buf();
1366                let mut contents = file.contents().to_vec();
1367
1368                // Check if file has .template extension (explicit template)
1369                let is_explicit_template = relative
1370                    .extension()
1371                    .map(|ext| ext == "template")
1372                    .unwrap_or(false);
1373
1374                // Check if file is a text file that should be processed for templates
1375                let should_render = is_explicit_template || is_template_file(&relative);
1376
1377                if is_explicit_template {
1378                    // Remove .template extension from output filename
1379                    relative.set_extension("");
1380                }
1381
1382                if should_render && let Ok(text) = std::str::from_utf8(&contents) {
1383                    let rendered = render_template(text, vars);
1384                    // Validate that all template variables were replaced
1385                    validate_no_unreplaced_placeholders(&rendered, &relative)?;
1386                    contents = rendered.into_bytes();
1387                }
1388
1389                let out_path = out_root.join(relative);
1390                if let Some(parent) = out_path.parent() {
1391                    fs::create_dir_all(parent)?;
1392                }
1393                fs::write(&out_path, contents)?;
1394            }
1395        }
1396    }
1397    Ok(())
1398}
1399
1400/// Checks if a file should be processed for template variable substitution
1401/// based on its extension
1402fn is_template_file(path: &Path) -> bool {
1403    // Check for .template extension on any file
1404    if let Some(ext) = path.extension() {
1405        if ext == "template" {
1406            return true;
1407        }
1408        // Check if the base extension is in our list
1409        if let Some(ext_str) = ext.to_str() {
1410            return TEMPLATE_EXTENSIONS.contains(&ext_str);
1411        }
1412    }
1413    // Also check the filename without the .template extension
1414    if let Some(stem) = path.file_stem() {
1415        let stem_path = Path::new(stem);
1416        if let Some(ext) = stem_path.extension()
1417            && let Some(ext_str) = ext.to_str()
1418        {
1419            return TEMPLATE_EXTENSIONS.contains(&ext_str);
1420        }
1421    }
1422    false
1423}
1424
1425/// Validates that no unreplaced template placeholders remain in the rendered content
1426fn validate_no_unreplaced_placeholders(content: &str, file_path: &Path) -> Result<(), BenchError> {
1427    // Find all {{...}} patterns
1428    let mut pos = 0;
1429    let mut unreplaced = Vec::new();
1430
1431    while let Some(start) = content[pos..].find("{{") {
1432        let abs_start = pos + start;
1433        if let Some(end) = content[abs_start..].find("}}") {
1434            let placeholder = &content[abs_start..abs_start + end + 2];
1435            // Extract just the variable name
1436            let var_name = &content[abs_start + 2..abs_start + end];
1437            // Skip placeholders that look like Gradle variable syntax (e.g., ${...})
1438            // or other non-template patterns
1439            if !var_name.contains('$') && !var_name.contains(' ') && !var_name.is_empty() {
1440                unreplaced.push(placeholder.to_string());
1441            }
1442            pos = abs_start + end + 2;
1443        } else {
1444            break;
1445        }
1446    }
1447
1448    if !unreplaced.is_empty() {
1449        return Err(BenchError::Build(format!(
1450            "Template validation failed for {:?}: unreplaced placeholders found: {:?}\n\n\
1451             This is a bug in mobench-sdk. Please report it at:\n\
1452             https://github.com/worldcoin/mobile-bench-rs/issues",
1453            file_path, unreplaced
1454        )));
1455    }
1456
1457    Ok(())
1458}
1459
1460fn render_template(input: &str, vars: &[TemplateVar]) -> String {
1461    let mut output = input.to_string();
1462    for var in vars {
1463        output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
1464    }
1465    output
1466}
1467
1468/// Sanitizes a string to be a valid iOS bundle identifier component
1469///
1470/// Bundle identifiers can only contain alphanumeric characters (A-Z, a-z, 0-9),
1471/// hyphens (-), and dots (.). However, to avoid issues and maintain consistency,
1472/// this function converts all non-alphanumeric characters to lowercase letters only.
1473///
1474/// Examples:
1475/// - "bench-mobile" -> "benchmobile"
1476/// - "bench_mobile" -> "benchmobile"
1477/// - "my-project_name" -> "myprojectname"
1478pub fn sanitize_bundle_id_component(name: &str) -> String {
1479    name.chars()
1480        .filter(|c| c.is_ascii_alphanumeric())
1481        .collect::<String>()
1482        .to_lowercase()
1483}
1484
1485fn sanitize_package_name(name: &str) -> String {
1486    name.chars()
1487        .map(|c| {
1488            if c.is_ascii_alphanumeric() {
1489                c.to_ascii_lowercase()
1490            } else {
1491                '-'
1492            }
1493        })
1494        .collect::<String>()
1495        .trim_matches('-')
1496        .replace("--", "-")
1497}
1498
1499/// Converts a string to PascalCase
1500pub fn to_pascal_case(input: &str) -> String {
1501    input
1502        .split(|c: char| !c.is_ascii_alphanumeric())
1503        .filter(|s| !s.is_empty())
1504        .map(|s| {
1505            let mut chars = s.chars();
1506            let first = chars.next().unwrap().to_ascii_uppercase();
1507            let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
1508            format!("{}{}", first, rest)
1509        })
1510        .collect::<String>()
1511}
1512
1513/// Checks if the Android project scaffolding exists at the given output directory
1514///
1515/// Returns true if the `android/build.gradle` or `android/build.gradle.kts` file exists.
1516pub fn android_project_exists(output_dir: &Path) -> bool {
1517    let android_dir = output_dir.join("android");
1518    android_dir.join("build.gradle").exists() || android_dir.join("build.gradle.kts").exists()
1519}
1520
1521/// Checks if the iOS project scaffolding exists at the given output directory
1522///
1523/// Returns true if the `ios/BenchRunner/project.yml` file exists.
1524pub fn ios_project_exists(output_dir: &Path) -> bool {
1525    output_dir.join("ios/BenchRunner/project.yml").exists()
1526}
1527
1528/// Checks whether an existing iOS project was generated for the given library name.
1529///
1530/// Returns `false` if the xcframework reference in `project.yml` doesn't match,
1531/// which means the project needs to be regenerated for the new crate.
1532fn ios_project_matches_library(output_dir: &Path, library_name: &str) -> bool {
1533    let project_yml = output_dir.join("ios/BenchRunner/project.yml");
1534    let Ok(content) = std::fs::read_to_string(&project_yml) else {
1535        return false;
1536    };
1537    let expected = format!("../{}.xcframework", library_name);
1538    content.contains(&expected)
1539}
1540
1541/// Checks whether an existing Android project was generated for the given library name.
1542///
1543/// Returns `false` if the JNI library name in `build.gradle` doesn't match,
1544/// which means the project needs to be regenerated for the new crate.
1545fn android_project_matches_library(output_dir: &Path, library_name: &str) -> bool {
1546    let build_gradle = output_dir.join("android/app/build.gradle");
1547    let Ok(content) = std::fs::read_to_string(&build_gradle) else {
1548        return false;
1549    };
1550    let expected = format!("lib{}.so", library_name);
1551    content.contains(&expected)
1552}
1553
1554/// Checks whether an existing Android runner was generated for the selected FFI backend.
1555fn android_project_matches_backend(output_dir: &Path, ffi_backend: crate::FfiBackend) -> bool {
1556    let Some(main_activity) = read_android_main_activity(output_dir) else {
1557        return false;
1558    };
1559
1560    match ffi_backend {
1561        crate::FfiBackend::Uniffi => {
1562            main_activity.contains("uniffi.") || main_activity.contains("runBenchmark(spec")
1563        }
1564        crate::FfiBackend::NativeCAbi => {
1565            main_activity.contains("mobench_run_benchmark_json")
1566                && main_activity.contains("com.sun.jna")
1567        }
1568        crate::FfiBackend::BoltFfi => main_activity.contains("runBenchmarkJson(specJson"),
1569    }
1570}
1571
1572fn read_android_main_activity(output_dir: &Path) -> Option<String> {
1573    let java_root = output_dir.join("android/app/src/main/java");
1574    let mut stack = vec![java_root];
1575    let mut content = String::new();
1576
1577    while let Some(dir) = stack.pop() {
1578        let entries = std::fs::read_dir(dir).ok()?;
1579        for entry in entries.flatten() {
1580            let path = entry.path();
1581            if path.is_dir() {
1582                stack.push(path);
1583            } else if path.file_name().and_then(|name| name.to_str()) == Some("MainActivity.kt") {
1584                let file_content = std::fs::read_to_string(path).ok()?;
1585                content.push_str(&file_content);
1586                content.push('\n');
1587            }
1588        }
1589    }
1590
1591    if content.is_empty() {
1592        None
1593    } else {
1594        Some(content)
1595    }
1596}
1597
1598/// Detects the first benchmark function in a crate by scanning src/lib.rs for `#[benchmark]`
1599///
1600/// This function looks for functions marked with the `#[benchmark]` attribute and returns
1601/// the first one found in the format `{crate_name}::{function_name}`.
1602///
1603/// # Arguments
1604///
1605/// * `crate_dir` - Path to the crate directory containing Cargo.toml
1606/// * `crate_name` - Name of the crate (used as prefix for the function name)
1607///
1608/// # Returns
1609///
1610/// * `Some(String)` - The detected function name in format `crate_name::function_name`
1611/// * `None` - If no benchmark functions are found or if the file cannot be read
1612pub fn detect_default_function(crate_dir: &Path, crate_name: &str) -> Option<String> {
1613    let lib_rs = crate_dir.join("src/lib.rs");
1614    if !lib_rs.exists() {
1615        return None;
1616    }
1617
1618    let file = fs::File::open(&lib_rs).ok()?;
1619    let reader = BufReader::new(file);
1620
1621    let mut found_benchmark_attr = false;
1622    let crate_name_normalized = crate_name.replace('-', "_");
1623
1624    for line in reader.lines().map_while(Result::ok) {
1625        let trimmed = line.trim();
1626
1627        // Check for #[benchmark] attribute
1628        if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
1629            found_benchmark_attr = true;
1630            continue;
1631        }
1632
1633        // If we found a benchmark attribute, look for the function definition
1634        if found_benchmark_attr {
1635            // Look for "fn function_name" or "pub fn function_name"
1636            if let Some(fn_pos) = trimmed.find("fn ") {
1637                let after_fn = &trimmed[fn_pos + 3..];
1638                // Extract function name (until '(' or whitespace)
1639                let fn_name: String = after_fn
1640                    .chars()
1641                    .take_while(|c| c.is_alphanumeric() || *c == '_')
1642                    .collect();
1643
1644                if !fn_name.is_empty() {
1645                    return Some(format!("{}::{}", crate_name_normalized, fn_name));
1646                }
1647            }
1648            // Reset if we hit a line that's not a function definition
1649            // (could be another attribute or comment)
1650            if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
1651                found_benchmark_attr = false;
1652            }
1653        }
1654    }
1655
1656    None
1657}
1658
1659/// Detects all benchmark functions in a crate by scanning src/lib.rs for `#[benchmark]`
1660///
1661/// This function looks for functions marked with the `#[benchmark]` attribute and returns
1662/// all found in the format `{crate_name}::{function_name}`.
1663///
1664/// # Arguments
1665///
1666/// * `crate_dir` - Path to the crate directory containing Cargo.toml
1667/// * `crate_name` - Name of the crate (used as prefix for the function names)
1668///
1669/// # Returns
1670///
1671/// A vector of benchmark function names in format `crate_name::function_name`
1672pub fn detect_all_benchmarks(crate_dir: &Path, crate_name: &str) -> Vec<String> {
1673    let lib_rs = crate_dir.join("src/lib.rs");
1674    if !lib_rs.exists() {
1675        return Vec::new();
1676    }
1677
1678    let Ok(file) = fs::File::open(&lib_rs) else {
1679        return Vec::new();
1680    };
1681    let reader = BufReader::new(file);
1682
1683    let mut benchmarks = Vec::new();
1684    let mut found_benchmark_attr = false;
1685    let crate_name_normalized = crate_name.replace('-', "_");
1686
1687    for line in reader.lines().map_while(Result::ok) {
1688        let trimmed = line.trim();
1689
1690        // Check for #[benchmark] attribute
1691        if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
1692            found_benchmark_attr = true;
1693            continue;
1694        }
1695
1696        // If we found a benchmark attribute, look for the function definition
1697        if found_benchmark_attr {
1698            // Look for "fn function_name" or "pub fn function_name"
1699            if let Some(fn_pos) = trimmed.find("fn ") {
1700                let after_fn = &trimmed[fn_pos + 3..];
1701                // Extract function name (until '(' or whitespace)
1702                let fn_name: String = after_fn
1703                    .chars()
1704                    .take_while(|c| c.is_alphanumeric() || *c == '_')
1705                    .collect();
1706
1707                if !fn_name.is_empty() {
1708                    benchmarks.push(format!("{}::{}", crate_name_normalized, fn_name));
1709                }
1710                found_benchmark_attr = false;
1711            }
1712            // Reset if we hit a line that's not a function definition
1713            // (could be another attribute or comment)
1714            if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
1715                found_benchmark_attr = false;
1716            }
1717        }
1718    }
1719
1720    benchmarks
1721}
1722
1723/// Validates that a benchmark function exists in the crate source
1724///
1725/// # Arguments
1726///
1727/// * `crate_dir` - Path to the crate directory containing Cargo.toml
1728/// * `crate_name` - Name of the crate (used as prefix for the function names)
1729/// * `function_name` - The function name to validate (with or without crate prefix)
1730///
1731/// # Returns
1732///
1733/// `true` if the function is found, `false` otherwise
1734pub fn validate_benchmark_exists(crate_dir: &Path, crate_name: &str, function_name: &str) -> bool {
1735    let benchmarks = detect_all_benchmarks(crate_dir, crate_name);
1736    let crate_name_normalized = crate_name.replace('-', "_");
1737
1738    // Normalize the function name - add crate prefix if missing
1739    let normalized_name = if function_name.contains("::") {
1740        function_name.to_string()
1741    } else {
1742        format!("{}::{}", crate_name_normalized, function_name)
1743    };
1744
1745    benchmarks.iter().any(|b| b == &normalized_name)
1746}
1747
1748/// Resolves the default benchmark function for a project
1749///
1750/// This function attempts to auto-detect benchmark functions from the crate's source.
1751/// If no benchmarks are found, it falls back to a sensible default based on the crate name.
1752///
1753/// # Arguments
1754///
1755/// * `project_root` - Root directory of the project
1756/// * `crate_name` - Name of the benchmark crate
1757/// * `crate_dir` - Optional explicit crate directory (if None, will search standard locations)
1758///
1759/// # Returns
1760///
1761/// The default function name in format `crate_name::function_name`
1762pub fn resolve_default_function(
1763    project_root: &Path,
1764    crate_name: &str,
1765    crate_dir: Option<&Path>,
1766) -> String {
1767    let crate_name_normalized = crate_name.replace('-', "_");
1768
1769    // Try to find the crate directory
1770    let search_dirs: Vec<PathBuf> = if let Some(dir) = crate_dir {
1771        vec![dir.to_path_buf()]
1772    } else {
1773        vec![
1774            project_root.join("bench-mobile"),
1775            project_root.join("crates").join(crate_name),
1776            project_root.to_path_buf(),
1777        ]
1778    };
1779
1780    // Try to detect benchmarks from each potential location
1781    for dir in &search_dirs {
1782        if dir.join("Cargo.toml").exists()
1783            && let Some(detected) = detect_default_function(dir, &crate_name_normalized)
1784        {
1785            return detected;
1786        }
1787    }
1788
1789    // Fallback: use a sensible default based on crate name
1790    format!("{}::example_benchmark", crate_name_normalized)
1791}
1792
1793/// Auto-generates Android project scaffolding from a crate name
1794///
1795/// This is a convenience function that derives template variables from the
1796/// crate name and generates the Android project structure. It auto-detects
1797/// the default benchmark function from the crate's source code.
1798///
1799/// # Arguments
1800///
1801/// * `output_dir` - Directory to write the `android/` project into
1802/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
1803pub fn ensure_android_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
1804    ensure_android_project_with_options(output_dir, crate_name, None, None)
1805}
1806
1807/// Auto-generates Android project scaffolding with additional options
1808///
1809/// This is a more flexible version of `ensure_android_project` that allows
1810/// specifying a custom default function and/or crate directory.
1811///
1812/// # Arguments
1813///
1814/// * `output_dir` - Directory to write the `android/` project into
1815/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
1816/// * `project_root` - Optional project root for auto-detecting benchmarks (defaults to output_dir parent)
1817/// * `crate_dir` - Optional explicit crate directory for benchmark detection
1818pub fn ensure_android_project_with_options(
1819    output_dir: &Path,
1820    crate_name: &str,
1821    project_root: Option<&Path>,
1822    crate_dir: Option<&Path>,
1823) -> Result<(), BenchError> {
1824    ensure_android_project_with_backend_options(
1825        output_dir,
1826        crate_name,
1827        project_root,
1828        crate_dir,
1829        crate::FfiBackend::Uniffi,
1830    )
1831}
1832
1833/// Auto-generates Android project scaffolding for the selected FFI backend.
1834pub fn ensure_android_project_with_backend_options(
1835    output_dir: &Path,
1836    crate_name: &str,
1837    project_root: Option<&Path>,
1838    crate_dir: Option<&Path>,
1839    ffi_backend: crate::FfiBackend,
1840) -> Result<(), BenchError> {
1841    let library_name = crate_name.replace('-', "_");
1842    let project_exists = android_project_exists(output_dir);
1843    let project_matches_library = android_project_matches_library(output_dir, &library_name);
1844    let project_matches_backend = android_project_matches_backend(output_dir, ffi_backend);
1845
1846    if project_exists && !project_matches_library {
1847        println!(
1848            "Existing Android scaffolding does not match library, regenerating for {} backend...",
1849            ffi_backend
1850        );
1851    } else if project_exists && !project_matches_backend {
1852        println!(
1853            "Existing Android scaffolding does not match FFI backend, regenerating for {} backend...",
1854            ffi_backend
1855        );
1856    } else if project_exists {
1857        println!(
1858            "Refreshing generated Android scaffolding for {} backend...",
1859            ffi_backend
1860        );
1861    } else {
1862        println!(
1863            "Android project not found, generating scaffolding for {} backend...",
1864            ffi_backend
1865        );
1866    }
1867
1868    let project_slug = crate_name.replace('-', "_");
1869
1870    // Resolve the default function by auto-detecting from source
1871    let effective_root = project_root.unwrap_or_else(|| output_dir.parent().unwrap_or(output_dir));
1872    let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
1873
1874    generate_android_project_with_backend(
1875        output_dir,
1876        &project_slug,
1877        &default_function,
1878        ffi_backend,
1879    )?;
1880    println!(
1881        "  Generated Android project at {:?}",
1882        output_dir.join("android")
1883    );
1884    println!("  Default benchmark function: {}", default_function);
1885    Ok(())
1886}
1887
1888/// Auto-generates iOS project scaffolding from a crate name
1889///
1890/// This is a convenience function that derives template variables from the
1891/// crate name and generates the iOS project structure. It auto-detects
1892/// the default benchmark function from the crate's source code.
1893///
1894/// # Arguments
1895///
1896/// * `output_dir` - Directory to write the `ios/` project into
1897/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
1898pub fn ensure_ios_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
1899    ensure_ios_project_with_options(output_dir, crate_name, None, None)
1900}
1901
1902/// Auto-generates iOS project scaffolding with additional options
1903///
1904/// This is a more flexible version of `ensure_ios_project` that allows
1905/// specifying a custom default function and/or crate directory.
1906///
1907/// # Arguments
1908///
1909/// * `output_dir` - Directory to write the `ios/` project into
1910/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
1911/// * `project_root` - Optional project root for auto-detecting benchmarks (defaults to output_dir parent)
1912/// * `crate_dir` - Optional explicit crate directory for benchmark detection
1913pub fn ensure_ios_project_with_options(
1914    output_dir: &Path,
1915    crate_name: &str,
1916    project_root: Option<&Path>,
1917    crate_dir: Option<&Path>,
1918) -> Result<(), BenchError> {
1919    ensure_ios_project_with_backend_options(
1920        output_dir,
1921        crate_name,
1922        project_root,
1923        crate_dir,
1924        crate::FfiBackend::Uniffi,
1925        IosProjectOptions::default(),
1926    )
1927}
1928
1929/// Auto-generates iOS project scaffolding for the selected FFI backend.
1930pub fn ensure_ios_project_with_backend_options(
1931    output_dir: &Path,
1932    crate_name: &str,
1933    project_root: Option<&Path>,
1934    crate_dir: Option<&Path>,
1935    ffi_backend: crate::FfiBackend,
1936    options: IosProjectOptions,
1937) -> Result<(), BenchError> {
1938    let library_name = crate_name.replace('-', "_");
1939    let project_exists = ios_project_exists(output_dir);
1940    let project_matches = ios_project_matches_library(output_dir, &library_name);
1941    if project_exists && !project_matches {
1942        println!(
1943            "Existing iOS scaffolding does not match library, regenerating for {} backend...",
1944            ffi_backend
1945        );
1946    } else if project_exists {
1947        println!(
1948            "Refreshing generated iOS scaffolding for {} backend...",
1949            ffi_backend
1950        );
1951    } else {
1952        println!(
1953            "iOS project not found, generating scaffolding for {} backend...",
1954            ffi_backend
1955        );
1956    }
1957
1958    // Use fixed "BenchRunner" for project/scheme name to match template directory structure
1959    let project_pascal = "BenchRunner";
1960    // Derive library name and bundle prefix from crate name
1961    let library_name = crate_name.replace('-', "_");
1962    // Use sanitized bundle ID component (alphanumeric only) to avoid iOS validation issues
1963    // e.g., "bench-mobile" or "bench_mobile" -> "benchmobile"
1964    let bundle_id_component = sanitize_bundle_id_component(crate_name);
1965    let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1966
1967    // Resolve the default function by auto-detecting from source
1968    let effective_root = project_root.unwrap_or_else(|| output_dir.parent().unwrap_or(output_dir));
1969    let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
1970
1971    generate_ios_project_with_backend_options(
1972        output_dir,
1973        &library_name,
1974        project_pascal,
1975        &bundle_prefix,
1976        &default_function,
1977        ffi_backend,
1978        options,
1979    )?;
1980    sync_ios_mobile_spec(output_dir)?;
1981    println!("  Generated iOS project at {:?}", output_dir.join("ios"));
1982    println!("  Default benchmark function: {}", default_function);
1983    Ok(())
1984}
1985
1986fn sync_ios_mobile_spec(output_dir: &Path) -> Result<(), BenchError> {
1987    let source = output_dir.join("target/mobile-spec/ios/bench_spec.json");
1988    if !source.is_file() {
1989        return Ok(());
1990    }
1991    let destination = output_dir.join("ios/BenchRunner/BenchRunner/Resources/bench_spec.json");
1992    let parent = destination.parent().ok_or_else(|| {
1993        BenchError::Build(format!(
1994            "Invalid iOS benchmark spec destination: {}",
1995            destination.display()
1996        ))
1997    })?;
1998    fs::create_dir_all(parent).map_err(|error| {
1999        BenchError::Build(format!(
2000            "Failed to create iOS benchmark resource directory {}: {error}",
2001            parent.display()
2002        ))
2003    })?;
2004    fs::copy(&source, &destination).map_err(|error| {
2005        BenchError::Build(format!(
2006            "Failed to copy iOS benchmark spec from {} to {}: {error}",
2007            source.display(),
2008            destination.display()
2009        ))
2010    })?;
2011    Ok(())
2012}
2013
2014#[cfg(test)]
2015mod tests {
2016    use super::*;
2017    use std::env;
2018
2019    #[test]
2020    fn test_generate_bench_mobile_crate() {
2021        let temp_dir = env::temp_dir().join("mobench-sdk-test");
2022        fs::create_dir_all(&temp_dir).unwrap();
2023
2024        let result = generate_bench_mobile_crate(&temp_dir, "test_project");
2025        assert!(result.is_ok());
2026
2027        // Verify files were created
2028        assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
2029        assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
2030        assert!(temp_dir.join("bench-mobile/build.rs").exists());
2031        let cargo_toml =
2032            fs::read_to_string(temp_dir.join("bench-mobile/Cargo.toml")).expect("read Cargo.toml");
2033        assert!(
2034            cargo_toml.contains(
2035                r#"mobench-sdk = { path = "..", default-features = false, features = ["registry"] }"#
2036            ),
2037            "generated FFI wrapper should depend on the narrow registry feature, got:\n{cargo_toml}"
2038        );
2039
2040        // Cleanup
2041        fs::remove_dir_all(&temp_dir).ok();
2042    }
2043
2044    #[test]
2045    fn test_generate_android_project_no_unreplaced_placeholders() {
2046        let temp_dir = env::temp_dir().join("mobench-sdk-android-test");
2047        // Clean up any previous test run
2048        let _ = fs::remove_dir_all(&temp_dir);
2049        fs::create_dir_all(&temp_dir).unwrap();
2050
2051        let result =
2052            generate_android_project(&temp_dir, "my-bench-project", "my_bench_project::test_func");
2053        assert!(
2054            result.is_ok(),
2055            "generate_android_project failed: {:?}",
2056            result.err()
2057        );
2058
2059        // Verify key files exist
2060        let android_dir = temp_dir.join("android");
2061        assert!(android_dir.join("settings.gradle").exists());
2062        assert!(android_dir.join("app/build.gradle").exists());
2063        assert!(
2064            android_dir
2065                .join("app/src/main/AndroidManifest.xml")
2066                .exists()
2067        );
2068        assert!(
2069            android_dir
2070                .join("app/src/main/res/values/strings.xml")
2071                .exists()
2072        );
2073        assert!(
2074            android_dir
2075                .join("app/src/main/res/values/themes.xml")
2076                .exists()
2077        );
2078
2079        // Verify no unreplaced placeholders remain in generated files
2080        let files_to_check = [
2081            "settings.gradle",
2082            "app/build.gradle",
2083            "app/src/main/AndroidManifest.xml",
2084            "app/src/main/res/values/strings.xml",
2085            "app/src/main/res/values/themes.xml",
2086        ];
2087
2088        for file in files_to_check {
2089            let path = android_dir.join(file);
2090            let contents =
2091                fs::read_to_string(&path).unwrap_or_else(|_| panic!("Failed to read {}", file));
2092
2093            // Check for unreplaced placeholders
2094            let has_placeholder = contents.contains("{{") && contents.contains("}}");
2095            assert!(
2096                !has_placeholder,
2097                "File {} contains unreplaced template placeholders: {}",
2098                file, contents
2099            );
2100        }
2101
2102        // Verify specific substitutions were made
2103        let settings = fs::read_to_string(android_dir.join("settings.gradle")).unwrap();
2104        assert!(
2105            settings.contains("my-bench-project-android")
2106                || settings.contains("my_bench_project-android"),
2107            "settings.gradle should contain project name"
2108        );
2109
2110        let build_gradle = fs::read_to_string(android_dir.join("app/build.gradle")).unwrap();
2111        // Package name should be sanitized (no hyphens/underscores) for consistency with iOS
2112        assert!(
2113            build_gradle.contains("dev.world.mybenchproject"),
2114            "build.gradle should contain sanitized package name 'dev.world.mybenchproject'"
2115        );
2116        assert!(
2117            !build_gradle.contains("testBuildType \"release\""),
2118            "debug builds should be able to produce assembleDebugAndroidTest"
2119        );
2120        assert!(
2121            build_gradle.contains("mobenchTestBuildType"),
2122            "release builds should be able to request assembleReleaseAndroidTest"
2123        );
2124
2125        let manifest =
2126            fs::read_to_string(android_dir.join("app/src/main/AndroidManifest.xml")).unwrap();
2127        assert!(
2128            manifest.contains("Theme.MyBenchProject"),
2129            "AndroidManifest.xml should contain PascalCase theme name"
2130        );
2131
2132        let strings =
2133            fs::read_to_string(android_dir.join("app/src/main/res/values/strings.xml")).unwrap();
2134        assert!(
2135            strings.contains("Benchmark"),
2136            "strings.xml should contain app name with Benchmark"
2137        );
2138
2139        // Verify Kotlin files are in the correct package directory structure
2140        // For package "dev.world.mybenchproject", files should be in "dev/world/mybenchproject/"
2141        let main_activity_path =
2142            android_dir.join("app/src/main/java/dev/world/mybenchproject/MainActivity.kt");
2143        assert!(
2144            main_activity_path.exists(),
2145            "MainActivity.kt should be in package directory: {:?}",
2146            main_activity_path
2147        );
2148
2149        let test_activity_path = android_dir
2150            .join("app/src/androidTest/java/dev/world/mybenchproject/MainActivityTest.kt");
2151        assert!(
2152            test_activity_path.exists(),
2153            "MainActivityTest.kt should be in package directory: {:?}",
2154            test_activity_path
2155        );
2156
2157        // Verify the files are NOT in the root java directory
2158        assert!(
2159            !android_dir
2160                .join("app/src/main/java/MainActivity.kt")
2161                .exists(),
2162            "MainActivity.kt should not be in root java directory"
2163        );
2164        assert!(
2165            !android_dir
2166                .join("app/src/androidTest/java/MainActivityTest.kt")
2167                .exists(),
2168            "MainActivityTest.kt should not be in root java directory"
2169        );
2170
2171        // Cleanup
2172        fs::remove_dir_all(&temp_dir).ok();
2173    }
2174
2175    #[test]
2176    fn test_generate_android_project_replaces_previous_package_tree() {
2177        let temp_dir = env::temp_dir().join("mobench-sdk-android-regenerate-test");
2178        let _ = fs::remove_dir_all(&temp_dir);
2179        fs::create_dir_all(&temp_dir).unwrap();
2180
2181        generate_android_project(&temp_dir, "ffi_benchmark", "ffi_benchmark::bench_fibonacci")
2182            .unwrap();
2183        let old_package_dir = temp_dir.join("android/app/src/main/java/dev/world/ffibenchmark");
2184        assert!(
2185            old_package_dir.exists(),
2186            "expected first package tree to exist"
2187        );
2188
2189        generate_android_project(
2190            &temp_dir,
2191            "basic_benchmark",
2192            "basic_benchmark::bench_fibonacci",
2193        )
2194        .unwrap();
2195
2196        let new_package_dir = temp_dir.join("android/app/src/main/java/dev/world/basicbenchmark");
2197        assert!(
2198            new_package_dir.exists(),
2199            "expected new package tree to exist"
2200        );
2201        assert!(
2202            !old_package_dir.exists(),
2203            "old package tree should be removed when regenerating the Android scaffold"
2204        );
2205
2206        fs::remove_dir_all(&temp_dir).ok();
2207    }
2208
2209    #[test]
2210    fn test_generate_android_native_backend_runner_template() {
2211        let temp_dir = env::temp_dir().join("mobench-sdk-android-native-test");
2212        let _ = fs::remove_dir_all(&temp_dir);
2213        fs::create_dir_all(&temp_dir).unwrap();
2214
2215        generate_android_project_with_backend(
2216            &temp_dir,
2217            "native_benchmark",
2218            "native_benchmark::bench_prove",
2219            crate::FfiBackend::NativeCAbi,
2220        )
2221        .unwrap();
2222
2223        let main_activity = fs::read_to_string(
2224            temp_dir.join("android/app/src/main/java/dev/world/nativebenchmark/MainActivity.kt"),
2225        )
2226        .unwrap();
2227        assert!(main_activity.contains("com.sun.jna.Native"));
2228        assert!(main_activity.contains("com.sun.jna.NativeLong"));
2229        assert!(main_activity.contains("mobench_run_benchmark_json"));
2230        assert!(main_activity.contains("specLen: NativeLong"));
2231        assert!(main_activity.contains("@JvmField var len: NativeLong = NativeLong(0)"));
2232        assert!(main_activity.contains("@JvmField var cap: NativeLong = NativeLong(0)"));
2233        assert!(!main_activity.contains("specLen: Long"));
2234        assert!(main_activity.contains("BENCH_JSON"));
2235        assert!(main_activity.contains("bench_spec.json"));
2236        assert!(main_activity.contains("logBenchJson(json.toString())"));
2237        assert!(main_activity.contains("fun benchmarkTimeoutSecs(): Long"));
2238        assert!(main_activity.contains("fun heartbeatIntervalSecs(): Long"));
2239        assert!(main_activity.contains("fun emitTimeoutFailureFromTest(): String"));
2240        assert!(
2241            !main_activity.contains("uniffi."),
2242            "native Android runner must not import UniFFI bindings:\n{}",
2243            main_activity
2244        );
2245        assert!(
2246            !main_activity.contains("runBenchmark("),
2247            "native Android runner must call the JSON C ABI, not UniFFI runBenchmark"
2248        );
2249        let package_manifest =
2250            fs::read_to_string(temp_dir.join("android/app/build.gradle")).unwrap();
2251        assert!(package_manifest.contains("net.java.dev.jna:jna"));
2252        assert!(!package_manifest.contains("uniffi"));
2253        let android_manifest =
2254            fs::read_to_string(temp_dir.join("android/app/src/main/AndroidManifest.xml")).unwrap();
2255        assert!(android_manifest.contains("android:name=\".MainActivity\""));
2256        assert!(!android_manifest.contains("uniffi"));
2257        assert!(!android_manifest.contains("{{"));
2258
2259        fs::remove_dir_all(&temp_dir).ok();
2260    }
2261
2262    #[test]
2263    fn test_generate_android_boltffi_backend_runner_template() {
2264        let temp_dir = env::temp_dir().join("mobench-sdk-android-boltffi-test");
2265        let _ = fs::remove_dir_all(&temp_dir);
2266        fs::create_dir_all(&temp_dir).unwrap();
2267
2268        generate_android_project_with_backend(
2269            &temp_dir,
2270            "bolt_benchmark",
2271            "bolt_benchmark::bench_prove",
2272            crate::FfiBackend::BoltFfi,
2273        )
2274        .unwrap();
2275
2276        let main_activity = fs::read_to_string(
2277            temp_dir.join("android/app/src/main/java/dev/world/boltbenchmark/MainActivity.kt"),
2278        )
2279        .unwrap();
2280        assert!(main_activity.contains("import com.mobench.boltbenchmark.runBenchmarkJson"));
2281        assert!(main_activity.contains("runBenchmarkJson(specJson = spec.toString())"));
2282        assert!(main_activity.contains("fun isBenchmarkComplete()"));
2283        assert!(main_activity.contains("BENCH_JSON"));
2284        assert!(
2285            !main_activity.contains("uniffi."),
2286            "BoltFFI Android runner must not import UniFFI bindings:\n{}",
2287            main_activity
2288        );
2289        assert!(
2290            !main_activity.contains("com.sun.jna"),
2291            "BoltFFI Android runner must not use the native C ABI JNA bridge"
2292        );
2293
2294        let boltffi_toml = fs::read_to_string(temp_dir.join("boltffi.toml")).unwrap();
2295        assert!(boltffi_toml.contains("name = \"bolt_benchmark\""));
2296        assert!(boltffi_toml.contains("crate = \"bolt_benchmark\""));
2297        assert!(boltffi_toml.contains("package = \"com.mobench.boltbenchmark\""));
2298        assert!(boltffi_toml.contains("desktop_loader = \"system\""));
2299        assert!(boltffi_toml.contains("output = \"target/mobench/android/app/src/main/java\""));
2300
2301        fs::remove_dir_all(&temp_dir).ok();
2302    }
2303
2304    #[test]
2305    fn test_ensure_android_project_regenerates_when_ffi_backend_changes() {
2306        let temp_dir = env::temp_dir().join("mobench-sdk-android-backend-switch-test");
2307        let _ = fs::remove_dir_all(&temp_dir);
2308        fs::create_dir_all(&temp_dir).unwrap();
2309
2310        generate_android_project_with_backend(
2311            &temp_dir,
2312            "switch_benchmark",
2313            "switch_benchmark::bench_prove",
2314            crate::FfiBackend::Uniffi,
2315        )
2316        .unwrap();
2317
2318        let main_activity_path =
2319            temp_dir.join("android/app/src/main/java/dev/world/switchbenchmark/MainActivity.kt");
2320        let uniffi_main = fs::read_to_string(&main_activity_path).unwrap();
2321        assert!(uniffi_main.contains("uniffi."));
2322
2323        ensure_android_project_with_backend_options(
2324            &temp_dir,
2325            "switch_benchmark",
2326            None,
2327            None,
2328            crate::FfiBackend::BoltFfi,
2329        )
2330        .unwrap();
2331
2332        let boltffi_main = fs::read_to_string(&main_activity_path).unwrap();
2333        assert!(boltffi_main.contains("runBenchmarkJson(specJson = spec.toString())"));
2334        assert!(
2335            !boltffi_main.contains("uniffi."),
2336            "backend changes should regenerate the Android runner:\n{}",
2337            boltffi_main
2338        );
2339
2340        fs::remove_dir_all(&temp_dir).ok();
2341    }
2342
2343    #[test]
2344    fn test_ensure_android_project_refreshes_existing_backend_scaffolding() {
2345        let temp_dir = env::temp_dir().join("mobench-sdk-android-refresh-test");
2346        let _ = fs::remove_dir_all(&temp_dir);
2347        fs::create_dir_all(&temp_dir).unwrap();
2348
2349        generate_android_project_with_backend(
2350            &temp_dir,
2351            "refresh_benchmark",
2352            "refresh_benchmark::bench_prove",
2353            crate::FfiBackend::BoltFfi,
2354        )
2355        .unwrap();
2356
2357        let main_activity_path =
2358            temp_dir.join("android/app/src/main/java/dev/world/refreshbenchmark/MainActivity.kt");
2359        let stale_main = fs::read_to_string(&main_activity_path)
2360            .unwrap()
2361            .replace("BENCH_JSON_START", "STALE_BENCH_JSON_START");
2362        fs::write(&main_activity_path, stale_main).unwrap();
2363
2364        ensure_android_project_with_backend_options(
2365            &temp_dir,
2366            "refresh_benchmark",
2367            None,
2368            None,
2369            crate::FfiBackend::BoltFfi,
2370        )
2371        .unwrap();
2372
2373        let refreshed_main = fs::read_to_string(&main_activity_path).unwrap();
2374        assert!(refreshed_main.contains("BENCH_JSON_START"));
2375        assert!(
2376            !refreshed_main.contains("STALE_BENCH_JSON_START"),
2377            "existing same-backend scaffolding should be refreshed:\n{}",
2378            refreshed_main
2379        );
2380
2381        fs::remove_dir_all(&temp_dir).ok();
2382    }
2383
2384    #[test]
2385    fn test_write_boltffi_config_can_target_explicit_output_dir() {
2386        let temp_dir = env::temp_dir().join("mobench-sdk-boltffi-config-output-test");
2387        let _ = fs::remove_dir_all(&temp_dir);
2388        fs::create_dir_all(&temp_dir).unwrap();
2389        let output_dir = temp_dir.join("custom-mobench-output");
2390
2391        write_boltffi_config_with_output_dir(
2392            &temp_dir,
2393            "bolt_benchmark",
2394            "bolt-benchmark",
2395            "BenchRunner",
2396            "com.mobench.boltbenchmark",
2397            &["arm64".to_string(), "x86_64".to_string()],
2398            &output_dir,
2399        )
2400        .unwrap();
2401
2402        let boltffi_toml = fs::read_to_string(temp_dir.join("boltffi.toml")).unwrap();
2403        assert!(boltffi_toml.contains(&format!(
2404            "output = \"{}\"",
2405            output_dir
2406                .join("android/app/src/main/java")
2407                .to_string_lossy()
2408        )));
2409        assert!(boltffi_toml.contains(&format!(
2410            "output = \"{}\"",
2411            output_dir
2412                .join("ios/BenchRunner/BenchRunner/Generated/BoltFFIGenerated")
2413                .to_string_lossy()
2414        )));
2415
2416        fs::remove_dir_all(&temp_dir).ok();
2417    }
2418
2419    #[test]
2420    fn test_is_template_file() {
2421        assert!(is_template_file(Path::new("settings.gradle")));
2422        assert!(is_template_file(Path::new("app/build.gradle")));
2423        assert!(is_template_file(Path::new("AndroidManifest.xml")));
2424        assert!(is_template_file(Path::new("strings.xml")));
2425        assert!(is_template_file(Path::new("MainActivity.kt.template")));
2426        assert!(is_template_file(Path::new("project.yml")));
2427        assert!(is_template_file(Path::new("Info.plist")));
2428        assert!(!is_template_file(Path::new("libfoo.so")));
2429        assert!(!is_template_file(Path::new("image.png")));
2430    }
2431
2432    #[test]
2433    fn test_mobile_templates_read_process_peak_memory_compatibly() {
2434        let android =
2435            include_str!("../templates/android/app/src/main/java/MainActivity.kt.template");
2436        assert!(
2437            !android.contains("sample.processPeakMemoryKb"),
2438            "Android template should not require generated bindings to expose processPeakMemoryKb"
2439        );
2440        assert!(
2441            !android.contains("it.processPeakMemoryKb"),
2442            "Android template should not require generated bindings to expose processPeakMemoryKb"
2443        );
2444        assert!(android.contains("optionalProcessPeakMemoryKb(sample)"));
2445        assert!(
2446            !android.contains("sample.cpuTimeMs"),
2447            "Android template should tolerate BenchSample without cpuTimeMs"
2448        );
2449        assert!(
2450            !android.contains("sample.peakMemoryKb"),
2451            "Android template should tolerate BenchSample without peakMemoryKb"
2452        );
2453        assert!(
2454            !android.contains("report.phases"),
2455            "Android template should tolerate BenchReport without phases"
2456        );
2457        assert!(android.contains("ProcessMemorySampler"));
2458        assert!(android.contains("sampleIntervalMs: Long = 1000L"));
2459        assert!(android.contains("/proc/self/smaps_rollup"));
2460        assert!(android.contains("class BenchmarkWorkerService : Service()"));
2461        assert!(android.contains("ResultReceiver(Handler(Looper.getMainLooper()))"));
2462        assert!(android.contains("startForegroundService(intent)"));
2463        assert!(android.contains("startForeground(FOREGROUND_NOTIFICATION_ID"));
2464        assert!(android.contains("fun isBenchmarkComplete()"));
2465        assert!(!android.contains("resultLatch.await"));
2466        assert!(android.contains("memory_process\", \"isolated_worker\""));
2467
2468        let android_test = include_str!(
2469            "../templates/android/app/src/androidTest/java/MainActivityTest.kt.template"
2470        );
2471        assert!(android_test.contains("Log.i(\"BenchRunnerTest\""));
2472        assert!(android_test.contains("Thread.sleep(pollMs)"));
2473        assert!(
2474            android_test.contains("TimeUnit.SECONDS.toMillis({{ANDROID_HEARTBEAT_INTERVAL_SECS}})")
2475        );
2476        assert!(
2477            android_test.contains("TimeUnit.SECONDS.toMillis({{ANDROID_BENCHMARK_TIMEOUT_SECS}})")
2478        );
2479        assert!(android_test.contains("activity.isBenchmarkComplete()"));
2480
2481        let ios_test = include_str!(
2482            "../templates/ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift.template"
2483        );
2484        assert!(
2485            ios_test.contains("\\\"error\\\""),
2486            "iOS XCUITest template should fail when the benchmark report is an error payload"
2487        );
2488
2489        let android_manifest =
2490            include_str!("../templates/android/app/src/main/AndroidManifest.xml");
2491        assert!(android_manifest.contains("android.permission.FOREGROUND_SERVICE"));
2492        assert!(android_manifest.contains("android.permission.FOREGROUND_SERVICE_DATA_SYNC"));
2493        assert!(android_manifest.contains("android:name=\".BenchmarkWorkerService\""));
2494        assert!(android_manifest.contains("android:foregroundServiceType=\"dataSync\""));
2495        assert!(android_manifest.contains("android:process=\":mobench_worker\""));
2496
2497        let android_build_gradle = include_str!("../templates/android/app/build.gradle");
2498        assert!(android_build_gradle.contains("generatedMainBenchSpec"));
2499        assert!(android_build_gradle.contains("if (!generatedMainBenchSpec.exists())"));
2500
2501        let ios =
2502            include_str!("../templates/ios/BenchRunner/BenchRunner/BenchRunnerFFI.swift.template");
2503        assert!(
2504            !ios.contains("sample.processPeakMemoryKb"),
2505            "iOS template should not require generated bindings to expose processPeakMemoryKb"
2506        );
2507        assert!(
2508            !ios.contains(r"\.processPeakMemoryKb"),
2509            "iOS template should not require generated bindings to expose processPeakMemoryKb"
2510        );
2511        assert!(ios.contains("optionalProcessPeakMemoryKb(sample)"));
2512        assert!(ios.contains("return [\n                \"name\": name,"));
2513        assert!(
2514            !ios.contains("sample.cpuTimeMs"),
2515            "iOS template should tolerate BenchSample without cpuTimeMs"
2516        );
2517        assert!(
2518            !ios.contains("sample.peakMemoryKb"),
2519            "iOS template should tolerate BenchSample without peakMemoryKb"
2520        );
2521        assert!(
2522            !ios.contains("report.phases"),
2523            "iOS template should tolerate BenchReport without phases"
2524        );
2525        assert!(ios.contains("compactMap { optionalProcessPeakMemoryKb($0) }"));
2526        assert!(ios.contains("ProcessMemorySampler"));
2527        assert!(ios.contains("currentProcessResidentMemoryKb"));
2528        assert!(ios.contains("task_info("));
2529        assert!(ios.contains("\"memory_process\": \"benchmark_app\""));
2530        assert!(ios.contains("generateJSONReport(report, runProcessPeakMemoryKb:"));
2531        assert!(ios.contains("processPeakSamplesKb.max() ?? runProcessPeakMemoryKb"));
2532    }
2533
2534    #[test]
2535    fn test_android_worker_result_codes_do_not_collide_with_activity_constants() {
2536        let generated_runner =
2537            include_str!("../templates/android/app/src/main/java/MainActivity.kt.template");
2538        let native_runner = include_str!("native_templates/android/MainActivity.kt.template");
2539
2540        for (runner, success_receiver) in [
2541            (generated_runner, "MobenchResultCode.SUCCESS ->"),
2542            (native_runner, "resultCode == MobenchResultCode.SUCCESS"),
2543        ] {
2544            assert!(
2545                runner.contains("private object MobenchResultCode"),
2546                "worker result codes must live behind a qualified namespace"
2547            );
2548            assert!(runner.contains(success_receiver));
2549            assert!(runner.contains(
2550                "if (result.errorMessage == null) MobenchResultCode.SUCCESS else MobenchResultCode.ERROR"
2551            ));
2552            assert!(
2553                !runner.contains("private const val RESULT_OK"),
2554                "Activity.RESULT_OK shadows an unqualified top-level RESULT_OK"
2555            );
2556        }
2557        assert!(generated_runner.contains("MobenchResultCode.HEARTBEAT ->"));
2558        assert!(generated_runner.contains("receiver?.send(MobenchResultCode.HEARTBEAT"));
2559        for runner in [generated_runner, native_runner] {
2560            assert!(runner.contains("watchdogHandler.postDelayed(watchdog"));
2561            assert!(runner.contains("override fun onDestroy()"));
2562            assert!(runner.contains("stopWatchdog()"));
2563            assert!(runner.contains("resultText?.text = message"));
2564            assert!(runner.contains("elapsedMs >= params.timeoutSecs * 1_000L"));
2565        }
2566        assert!(
2567            !generated_runner.contains("benchmarkComplete || workerPid == null"),
2568            "the watchdog must detect a worker killed before its first heartbeat"
2569        );
2570        assert!(native_runner.contains("MobenchResultCode.HEARTBEAT"));
2571        assert!(native_runner.contains("fun checkWorkerExit()"));
2572        assert!(
2573            native_runner.contains("Benchmark worker process exited before BENCH_JSON was emitted")
2574        );
2575        assert!(native_runner.contains("catch (e: Throwable)"));
2576        assert!(native_runner.contains("RESULT_FAILURE_JSON_EXTRA"));
2577        assert!(native_runner.contains("BENCH_FAILURE_JSON $payload"));
2578        assert!(native_runner.contains("result.failureJson?.let"));
2579    }
2580
2581    #[test]
2582    fn test_android_runners_emit_collectable_chunked_benchmark_reports() {
2583        let generated_runner =
2584            include_str!("../templates/android/app/src/main/java/MainActivity.kt.template");
2585        let native_runner = include_str!("native_templates/android/MainActivity.kt.template");
2586        let boltffi_runner = include_str!("boltffi_templates/android/MainActivity.kt.template");
2587
2588        for runner in [generated_runner, native_runner, boltffi_runner] {
2589            assert!(runner.contains("BENCH_JSON_START"));
2590            assert!(runner.contains("BENCH_JSON_CHUNK"));
2591            assert!(runner.contains("BENCH_JSON_END"));
2592            assert!(runner.contains("val chunkSize = 1000"));
2593            assert!(runner.contains("Character.isHighSurrogate"));
2594        }
2595    }
2596
2597    #[test]
2598    fn generated_android_test_uses_configured_timeout_and_heartbeat() {
2599        let temp_dir = env::temp_dir().join("mobench-sdk-android-timeout-test");
2600        let _ = fs::remove_dir_all(&temp_dir);
2601        fs::create_dir_all(&temp_dir).expect("create temp dir");
2602
2603        unsafe {
2604            std::env::set_var("MOBENCH_ANDROID_BENCHMARK_TIMEOUT_SECS", "7200");
2605            std::env::set_var("MOBENCH_ANDROID_HEARTBEAT_INTERVAL_SECS", "15");
2606        }
2607        let result = generate_android_project_with_backend(
2608            &temp_dir,
2609            "my-bench-project",
2610            "sample_fns::fibonacci",
2611            crate::FfiBackend::NativeCAbi,
2612        );
2613        unsafe {
2614            std::env::remove_var("MOBENCH_ANDROID_BENCHMARK_TIMEOUT_SECS");
2615            std::env::remove_var("MOBENCH_ANDROID_HEARTBEAT_INTERVAL_SECS");
2616        }
2617        result.expect("generate Android project");
2618
2619        let android_test =
2620            fs::read_to_string(temp_dir.join(
2621                "android/app/src/androidTest/java/dev/world/mybenchproject/MainActivityTest.kt",
2622            ))
2623            .expect("read MainActivityTest.kt");
2624
2625        assert!(android_test.contains("TimeUnit.SECONDS.toMillis(7200)"));
2626        assert!(android_test.contains("TimeUnit.SECONDS.toMillis(15)"));
2627        let _ = fs::remove_dir_all(&temp_dir);
2628    }
2629
2630    #[test]
2631    fn test_validate_no_unreplaced_placeholders() {
2632        // Should pass with no placeholders
2633        assert!(validate_no_unreplaced_placeholders("hello world", Path::new("test.txt")).is_ok());
2634
2635        // Should pass with Gradle variables (not our placeholders)
2636        assert!(validate_no_unreplaced_placeholders("${ENV_VAR}", Path::new("test.txt")).is_ok());
2637
2638        // Should fail with unreplaced template placeholders
2639        let result = validate_no_unreplaced_placeholders("hello {{NAME}}", Path::new("test.txt"));
2640        assert!(result.is_err());
2641        let err = result.unwrap_err().to_string();
2642        assert!(err.contains("{{NAME}}"));
2643    }
2644
2645    #[test]
2646    fn test_to_pascal_case() {
2647        assert_eq!(to_pascal_case("my-project"), "MyProject");
2648        assert_eq!(to_pascal_case("my_project"), "MyProject");
2649        assert_eq!(to_pascal_case("myproject"), "Myproject");
2650        assert_eq!(to_pascal_case("my-bench-project"), "MyBenchProject");
2651    }
2652
2653    #[test]
2654    fn test_detect_default_function_finds_benchmark() {
2655        let temp_dir = env::temp_dir().join("mobench-sdk-detect-test");
2656        let _ = fs::remove_dir_all(&temp_dir);
2657        fs::create_dir_all(temp_dir.join("src")).unwrap();
2658
2659        // Create a lib.rs with a benchmark function
2660        let lib_content = r#"
2661use mobench_sdk::benchmark;
2662
2663/// Some docs
2664#[benchmark]
2665fn my_benchmark_func() {
2666    // benchmark code
2667}
2668
2669fn helper_func() {}
2670"#;
2671        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
2672        fs::write(temp_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
2673
2674        let result = detect_default_function(&temp_dir, "my_crate");
2675        assert_eq!(result, Some("my_crate::my_benchmark_func".to_string()));
2676
2677        // Cleanup
2678        fs::remove_dir_all(&temp_dir).ok();
2679    }
2680
2681    #[test]
2682    fn test_detect_default_function_no_benchmark() {
2683        let temp_dir = env::temp_dir().join("mobench-sdk-detect-none-test");
2684        let _ = fs::remove_dir_all(&temp_dir);
2685        fs::create_dir_all(temp_dir.join("src")).unwrap();
2686
2687        // Create a lib.rs without benchmark functions
2688        let lib_content = r#"
2689fn regular_function() {
2690    // no benchmark here
2691}
2692"#;
2693        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
2694
2695        let result = detect_default_function(&temp_dir, "my_crate");
2696        assert!(result.is_none());
2697
2698        // Cleanup
2699        fs::remove_dir_all(&temp_dir).ok();
2700    }
2701
2702    #[test]
2703    fn test_detect_default_function_pub_fn() {
2704        let temp_dir = env::temp_dir().join("mobench-sdk-detect-pub-test");
2705        let _ = fs::remove_dir_all(&temp_dir);
2706        fs::create_dir_all(temp_dir.join("src")).unwrap();
2707
2708        // Create a lib.rs with a public benchmark function
2709        let lib_content = r#"
2710#[benchmark]
2711pub fn public_bench() {
2712    // benchmark code
2713}
2714"#;
2715        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
2716
2717        let result = detect_default_function(&temp_dir, "test-crate");
2718        assert_eq!(result, Some("test_crate::public_bench".to_string()));
2719
2720        // Cleanup
2721        fs::remove_dir_all(&temp_dir).ok();
2722    }
2723
2724    #[test]
2725    fn test_resolve_default_function_fallback() {
2726        let temp_dir = env::temp_dir().join("mobench-sdk-resolve-test");
2727        let _ = fs::remove_dir_all(&temp_dir);
2728        fs::create_dir_all(&temp_dir).unwrap();
2729
2730        // No lib.rs exists, should fall back to default
2731        let result = resolve_default_function(&temp_dir, "my-crate", None);
2732        assert_eq!(result, "my_crate::example_benchmark");
2733
2734        // Cleanup
2735        fs::remove_dir_all(&temp_dir).ok();
2736    }
2737
2738    #[test]
2739    fn test_sanitize_bundle_id_component() {
2740        // Hyphens should be removed
2741        assert_eq!(sanitize_bundle_id_component("bench-mobile"), "benchmobile");
2742        // Underscores should be removed
2743        assert_eq!(sanitize_bundle_id_component("bench_mobile"), "benchmobile");
2744        // Mixed separators should all be removed
2745        assert_eq!(
2746            sanitize_bundle_id_component("my-project_name"),
2747            "myprojectname"
2748        );
2749        // Already valid should remain unchanged (but lowercase)
2750        assert_eq!(sanitize_bundle_id_component("benchmobile"), "benchmobile");
2751        // Numbers should be preserved
2752        assert_eq!(sanitize_bundle_id_component("bench2mobile"), "bench2mobile");
2753        // Uppercase should be lowercased
2754        assert_eq!(sanitize_bundle_id_component("BenchMobile"), "benchmobile");
2755        // Complex case
2756        assert_eq!(
2757            sanitize_bundle_id_component("My-Complex_Project-123"),
2758            "mycomplexproject123"
2759        );
2760    }
2761
2762    #[test]
2763    fn test_generate_ios_project_bundle_id_not_duplicated() {
2764        let temp_dir = env::temp_dir().join("mobench-sdk-ios-bundle-test");
2765        // Clean up any previous test run
2766        let _ = fs::remove_dir_all(&temp_dir);
2767        fs::create_dir_all(&temp_dir).unwrap();
2768
2769        // Use a crate name that would previously cause duplication
2770        let crate_name = "bench-mobile";
2771        let bundle_prefix = "dev.world.benchmobile";
2772        let project_pascal = "BenchRunner";
2773
2774        let result = generate_ios_project(
2775            &temp_dir,
2776            crate_name,
2777            project_pascal,
2778            bundle_prefix,
2779            "bench_mobile::test_func",
2780        );
2781        assert!(
2782            result.is_ok(),
2783            "generate_ios_project failed: {:?}",
2784            result.err()
2785        );
2786
2787        // Verify project.yml was created
2788        let project_yml_path = temp_dir.join("ios/BenchRunner/project.yml");
2789        assert!(project_yml_path.exists(), "project.yml should exist");
2790
2791        // Read and verify the bundle ID is correct (not duplicated)
2792        let project_yml = fs::read_to_string(&project_yml_path).unwrap();
2793
2794        // The bundle ID should be "dev.world.benchmobile.BenchRunner"
2795        // NOT "dev.world.benchmobile.benchmobile"
2796        assert!(
2797            project_yml.contains("dev.world.benchmobile.BenchRunner"),
2798            "Bundle ID should be 'dev.world.benchmobile.BenchRunner', got:\n{}",
2799            project_yml
2800        );
2801        assert!(
2802            !project_yml.contains("dev.world.benchmobile.benchmobile"),
2803            "Bundle ID should NOT be duplicated as 'dev.world.benchmobile.benchmobile', got:\n{}",
2804            project_yml
2805        );
2806        assert!(
2807            project_yml.contains("embed: false"),
2808            "Static xcframework dependency should be link-only, got:\n{}",
2809            project_yml
2810        );
2811
2812        // Cleanup
2813        fs::remove_dir_all(&temp_dir).ok();
2814    }
2815
2816    #[test]
2817    fn test_generate_ios_project_preserves_existing_resources_on_regeneration() {
2818        let temp_dir = env::temp_dir().join("mobench-sdk-ios-resources-regenerate-test");
2819        let _ = fs::remove_dir_all(&temp_dir);
2820        fs::create_dir_all(&temp_dir).unwrap();
2821
2822        generate_ios_project(
2823            &temp_dir,
2824            "bench_mobile",
2825            "BenchRunner",
2826            "dev.world.benchmobile",
2827            "bench_mobile::bench_prepare",
2828        )
2829        .unwrap();
2830
2831        let resources_dir = temp_dir.join("ios/BenchRunner/BenchRunner/Resources");
2832        fs::create_dir_all(resources_dir.join("nested")).unwrap();
2833        fs::write(
2834            resources_dir.join("bench_spec.json"),
2835            r#"{"function":"bench_mobile::bench_prove","iterations":2,"warmup":1}"#,
2836        )
2837        .unwrap();
2838        fs::write(
2839            resources_dir.join("bench_meta.json"),
2840            r#"{"build_id":"build-123"}"#,
2841        )
2842        .unwrap();
2843        fs::write(resources_dir.join("nested/custom.txt"), "keep me").unwrap();
2844
2845        generate_ios_project(
2846            &temp_dir,
2847            "bench_mobile",
2848            "BenchRunner",
2849            "dev.world.benchmobile",
2850            "bench_mobile::bench_prepare",
2851        )
2852        .unwrap();
2853
2854        assert_eq!(
2855            fs::read_to_string(resources_dir.join("bench_spec.json")).unwrap(),
2856            r#"{"function":"bench_mobile::bench_prove","iterations":2,"warmup":1}"#
2857        );
2858        assert_eq!(
2859            fs::read_to_string(resources_dir.join("bench_meta.json")).unwrap(),
2860            r#"{"build_id":"build-123"}"#
2861        );
2862        assert_eq!(
2863            fs::read_to_string(resources_dir.join("nested/custom.txt")).unwrap(),
2864            "keep me"
2865        );
2866
2867        fs::remove_dir_all(&temp_dir).ok();
2868    }
2869
2870    #[test]
2871    fn test_ensure_ios_project_copies_persisted_mobile_spec_into_resources() {
2872        let temp_dir = env::temp_dir().join("mobench-sdk-ios-persisted-spec-test");
2873        let _ = fs::remove_dir_all(&temp_dir);
2874        let source = temp_dir.join("target/mobile-spec/ios/bench_spec.json");
2875        fs::create_dir_all(source.parent().unwrap()).unwrap();
2876        let expected = r#"{"function":"bench_mobile::bench_prove","iterations":1,"warmup":0}"#;
2877        fs::write(&source, expected).unwrap();
2878
2879        ensure_ios_project_with_backend_options(
2880            &temp_dir,
2881            "bench-mobile",
2882            None,
2883            None,
2884            crate::FfiBackend::NativeCAbi,
2885            IosProjectOptions::default(),
2886        )
2887        .unwrap();
2888
2889        assert_eq!(
2890            fs::read_to_string(
2891                temp_dir.join("ios/BenchRunner/BenchRunner/Resources/bench_spec.json")
2892            )
2893            .unwrap(),
2894            expected
2895        );
2896
2897        fs::remove_dir_all(&temp_dir).ok();
2898    }
2899
2900    #[test]
2901    fn test_generate_ios_native_backend_runner_template() {
2902        let temp_dir = env::temp_dir().join("mobench-sdk-ios-native-test");
2903        let _ = fs::remove_dir_all(&temp_dir);
2904        fs::create_dir_all(&temp_dir).unwrap();
2905
2906        generate_ios_project_with_backend_options(
2907            &temp_dir,
2908            "native_benchmark",
2909            "BenchRunner",
2910            "dev.world.nativebenchmark",
2911            "native_benchmark::bench_prove",
2912            crate::FfiBackend::NativeCAbi,
2913            IosProjectOptions {
2914                runner: IosRunner::UikitLegacy,
2915                ..IosProjectOptions::default()
2916            },
2917        )
2918        .unwrap();
2919
2920        let ffi =
2921            fs::read_to_string(temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunnerFFI.swift"))
2922                .unwrap();
2923        assert!(ffi.contains("mobench_run_benchmark_json"));
2924        assert!(ffi.contains("mobench_free_buf"));
2925        assert!(ffi.contains("BENCH_FUNCTION"));
2926        assert!(
2927            !ffi.contains("runBenchmark(spec:"),
2928            "native iOS runner must call the JSON C ABI, not UniFFI runBenchmark"
2929        );
2930        assert!(
2931            !ffi.contains("let report: BenchReport"),
2932            "native iOS runner should operate on JSON, not UniFFI BenchReport"
2933        );
2934
2935        let header = fs::read_to_string(
2936            temp_dir.join("ios/BenchRunner/BenchRunner/Generated/native_benchmarkFFI.h"),
2937        )
2938        .unwrap();
2939        assert!(header.contains("mobench_run_benchmark_json"));
2940        let bridging_header = fs::read_to_string(
2941            temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunner-Bridging-Header.h"),
2942        )
2943        .unwrap();
2944        assert!(bridging_header.contains("#import \"native_benchmarkFFI.h\""));
2945        let package_manifest =
2946            fs::read_to_string(temp_dir.join("ios/BenchRunner/project.yml")).unwrap();
2947        assert!(package_manifest.contains("../native_benchmark.xcframework"));
2948        assert!(package_manifest.contains("SWIFT_OBJC_BRIDGING_HEADER"));
2949        assert!(!package_manifest.contains("uniffi"));
2950
2951        let runner = fs::read_to_string(
2952            temp_dir.join("ios/BenchRunner/BenchRunner/UIKitLegacyRunner.swift"),
2953        )
2954        .unwrap();
2955        assert!(runner.contains("jsonLabel.accessibilityValue = result.jsonReport"));
2956        assert!(runner.contains("completionLabel.accessibilityLabel = \"completed\""));
2957        assert!(
2958            runner.contains("heartbeatButton.accessibilityIdentifier = \"benchmarkHeartbeat\"")
2959        );
2960        assert!(runner.contains("@objc private func heartbeatTapped()"));
2961
2962        fs::remove_dir_all(&temp_dir).ok();
2963    }
2964
2965    #[test]
2966    fn test_generate_ios_boltffi_backend_runner_template() {
2967        let temp_dir = env::temp_dir().join("mobench-sdk-ios-boltffi-test");
2968        let _ = fs::remove_dir_all(&temp_dir);
2969        fs::create_dir_all(&temp_dir).unwrap();
2970
2971        generate_ios_project_with_backend(
2972            &temp_dir,
2973            "bolt_benchmark",
2974            "BenchRunner",
2975            "dev.world.boltbenchmark",
2976            "bolt_benchmark::bench_prove",
2977            crate::FfiBackend::BoltFfi,
2978        )
2979        .unwrap();
2980
2981        let ffi =
2982            fs::read_to_string(temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunnerFFI.swift"))
2983                .unwrap();
2984        assert!(ffi.contains("runBenchmarkJson(specJson:"));
2985        assert!(ffi.contains("BENCH_FUNCTION"));
2986        assert!(
2987            !ffi.contains("runBenchmark(spec:"),
2988            "BoltFFI iOS runner must call BoltFFI JSON bindings, not UniFFI runBenchmark"
2989        );
2990        assert!(
2991            !ffi.contains("mobench_run_benchmark_json"),
2992            "BoltFFI iOS runner must not call the native C ABI bridge"
2993        );
2994        let bridging_header = fs::read_to_string(
2995            temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunner-Bridging-Header.h"),
2996        )
2997        .unwrap();
2998        assert!(
2999            !bridging_header.contains("#import"),
3000            "BoltFFI iOS runner should import the generated C module from Swift, not a UniFFI-style bridging header"
3001        );
3002
3003        let boltffi_toml = fs::read_to_string(temp_dir.join("boltffi.toml")).unwrap();
3004        assert!(boltffi_toml.contains("module_name = \"BenchRunner\""));
3005        assert!(boltffi_toml.contains("ffi_module_name = \"bolt_benchmarkFFI\""));
3006        assert!(boltffi_toml.contains(
3007            "output = \"target/mobench/ios/BenchRunner/BenchRunner/Generated/BoltFFIGenerated\""
3008        ));
3009
3010        fs::remove_dir_all(&temp_dir).ok();
3011    }
3012
3013    #[test]
3014    fn test_ensure_ios_project_refreshes_existing_content_view_template() {
3015        let temp_dir = env::temp_dir().join("mobench-sdk-ios-refresh-test");
3016        let _ = fs::remove_dir_all(&temp_dir);
3017        fs::create_dir_all(&temp_dir).unwrap();
3018
3019        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3020            .expect("initial iOS project generation should succeed");
3021
3022        let content_view_path = temp_dir.join("ios/BenchRunner/BenchRunner/ContentView.swift");
3023        assert!(content_view_path.exists(), "ContentView.swift should exist");
3024
3025        fs::write(&content_view_path, "stale generated content").unwrap();
3026
3027        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3028            .expect("refreshing existing iOS project should succeed");
3029
3030        let refreshed = fs::read_to_string(&content_view_path).unwrap();
3031        assert!(
3032            refreshed.contains("ProfileLaunchOptions"),
3033            "refreshed ContentView.swift should contain the latest profiling template, got:\n{}",
3034            refreshed
3035        );
3036        assert!(
3037            refreshed.contains("repeatUntilMs"),
3038            "refreshed ContentView.swift should contain repeat-until profiling support, got:\n{}",
3039            refreshed
3040        );
3041        assert!(
3042            refreshed.contains("Task.detached(priority: .userInitiated)"),
3043            "refreshed ContentView.swift should run benchmarks off the main actor, got:\n{}",
3044            refreshed
3045        );
3046        assert!(
3047            refreshed.contains("await MainActor.run"),
3048            "refreshed ContentView.swift should apply UI updates on the main actor, got:\n{}",
3049            refreshed
3050        );
3051        assert!(
3052            refreshed.contains(".accessibilityLabel(reportJSON)"),
3053            "refreshed ContentView.swift should expose report JSON to XCUITest, got:\n{}",
3054            refreshed
3055        );
3056        assert!(
3057            refreshed.contains(".accessibilityValue(reportJSON)"),
3058            "refreshed ContentView.swift should expose report JSON as an accessibility value, got:\n{}",
3059            refreshed
3060        );
3061        assert!(
3062            refreshed.contains(".frame(width: 1, height: 1)"),
3063            "refreshed ContentView.swift should keep the report element in the accessibility tree, got:\n{}",
3064            refreshed
3065        );
3066        assert!(
3067            refreshed.contains(".opacity(0.01)"),
3068            "refreshed ContentView.swift should keep the report element nearly transparent, got:\n{}",
3069            refreshed
3070        );
3071        assert!(
3072            refreshed.contains(".accessibilityIdentifier(\"benchmarkHeartbeat\")"),
3073            "refreshed ContentView.swift should expose a remote-session heartbeat control, got:\n{}",
3074            refreshed
3075        );
3076
3077        fs::remove_dir_all(&temp_dir).ok();
3078    }
3079
3080    #[test]
3081    fn test_ensure_ios_project_refreshes_existing_ui_test_timeout_template() {
3082        let temp_dir = env::temp_dir().join("mobench-sdk-ios-uitest-refresh-test");
3083        let _ = fs::remove_dir_all(&temp_dir);
3084        fs::create_dir_all(&temp_dir).unwrap();
3085
3086        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3087            .expect("initial iOS project generation should succeed");
3088
3089        let ui_test_path =
3090            temp_dir.join("ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift");
3091        assert!(
3092            ui_test_path.exists(),
3093            "BenchRunnerUITests.swift should exist"
3094        );
3095
3096        fs::write(&ui_test_path, "stale generated content").unwrap();
3097
3098        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3099            .expect("refreshing existing iOS project should succeed");
3100
3101        let refreshed = fs::read_to_string(&ui_test_path).unwrap();
3102        assert!(
3103            refreshed.contains("private let defaultBenchmarkTimeout: TimeInterval = 300.0"),
3104            "refreshed BenchRunnerUITests.swift should include the default timeout, got:\n{}",
3105            refreshed
3106        );
3107        assert!(
3108            refreshed.contains(
3109                "ProcessInfo.processInfo.environment[\"MOBENCH_IOS_BENCHMARK_TIMEOUT_SECS\"]"
3110            ),
3111            "refreshed BenchRunnerUITests.swift should honor runtime timeout overrides, got:\n{}",
3112            refreshed
3113        );
3114        assert!(
3115            refreshed.contains("waitForBenchmarkCompletion(completedIndicator, app: app)"),
3116            "refreshed BenchRunnerUITests.swift should use heartbeat polling, got:\n{}",
3117            refreshed
3118        );
3119        assert!(
3120            refreshed.contains("NSPredicate(format: \"label == %@\", \"completed\")"),
3121            "refreshed BenchRunnerUITests.swift should wait for the completed state, got:\n{}",
3122            refreshed
3123        );
3124        assert!(
3125            refreshed
3126                .contains("XCTWaiter.wait(for: [completedExpectation], timeout: waitInterval)"),
3127            "refreshed BenchRunnerUITests.swift should predicate-wait without busy polling, got:\n{}",
3128            refreshed
3129        );
3130        assert!(
3131            refreshed.contains("MOBENCH_HEARTBEAT waiting for benchmark completion"),
3132            "refreshed BenchRunnerUITests.swift should emit heartbeat activity, got:\n{}",
3133            refreshed
3134        );
3135        assert!(
3136            refreshed.contains("heartbeatControl.tap()"),
3137            "refreshed BenchRunnerUITests.swift should keep remote sessions active with app interaction, got:\n{}",
3138            refreshed
3139        );
3140        assert!(
3141            refreshed.contains("reportElement.value as? String"),
3142            "refreshed BenchRunnerUITests.swift should read report JSON from the accessibility value, got:\n{}",
3143            refreshed
3144        );
3145        assert!(
3146            refreshed.contains("firstValidJSON([reportValue, reportElement.label])"),
3147            "refreshed BenchRunnerUITests.swift should fall back across valid accessibility JSON channels, got:\n{}",
3148            refreshed
3149        );
3150        assert!(
3151            refreshed.contains("JSONSerialization.jsonObject(with: data)"),
3152            "refreshed BenchRunnerUITests.swift should reject corrupt accessibility values, got:\n{}",
3153            refreshed
3154        );
3155        assert!(
3156            !refreshed.contains("completedIndicator.waitForExistence"),
3157            "refreshed BenchRunnerUITests.swift should not treat marker existence as completion, got:\n{}",
3158            refreshed
3159        );
3160
3161        fs::remove_dir_all(&temp_dir).ok();
3162    }
3163
3164    #[test]
3165    fn test_generate_ios_project_uses_configured_benchmark_timeout() {
3166        let temp_dir = env::temp_dir().join("mobench-sdk-ios-timeout-test");
3167        let _ = fs::remove_dir_all(&temp_dir);
3168        fs::create_dir_all(&temp_dir).unwrap();
3169
3170        let result = generate_ios_project_with_timeout(
3171            &temp_dir,
3172            "sample_fns",
3173            "BenchRunner",
3174            "dev.world.samplefns",
3175            "sample_fns::example_benchmark",
3176            1200,
3177            crate::FfiBackend::Uniffi,
3178        );
3179
3180        assert!(result.is_ok(), "generate_ios_project should succeed");
3181
3182        let ui_test_path =
3183            temp_dir.join("ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift");
3184        let contents = fs::read_to_string(&ui_test_path).unwrap();
3185        assert!(
3186            contents.contains("private let defaultBenchmarkTimeout: TimeInterval = 1200.0"),
3187            "generated BenchRunnerUITests.swift should embed the configured timeout, got:\n{}",
3188            contents
3189        );
3190
3191        fs::remove_dir_all(&temp_dir).ok();
3192    }
3193
3194    #[test]
3195    fn test_resolve_ios_benchmark_timeout_secs_defaults_invalid_values() {
3196        assert_eq!(resolve_ios_benchmark_timeout_secs(None), 300);
3197        assert_eq!(resolve_ios_benchmark_timeout_secs(Some("900")), 900);
3198        assert_eq!(resolve_ios_benchmark_timeout_secs(Some("0")), 300);
3199        assert_eq!(resolve_ios_benchmark_timeout_secs(Some("bogus")), 300);
3200    }
3201
3202    #[test]
3203    fn test_cross_platform_naming_consistency() {
3204        // Test that Android and iOS use the same naming convention for package/bundle IDs
3205        let temp_dir = env::temp_dir().join("mobench-sdk-naming-consistency-test");
3206        let _ = fs::remove_dir_all(&temp_dir);
3207        fs::create_dir_all(&temp_dir).unwrap();
3208
3209        let project_name = "bench-mobile";
3210
3211        // Generate Android project
3212        let result = generate_android_project(&temp_dir, project_name, "bench_mobile::test_func");
3213        assert!(
3214            result.is_ok(),
3215            "generate_android_project failed: {:?}",
3216            result.err()
3217        );
3218
3219        // Generate iOS project (mimicking how ensure_ios_project does it)
3220        let bundle_id_component = sanitize_bundle_id_component(project_name);
3221        let bundle_prefix = format!("dev.world.{}", bundle_id_component);
3222        let result = generate_ios_project(
3223            &temp_dir,
3224            &project_name.replace('-', "_"),
3225            "BenchRunner",
3226            &bundle_prefix,
3227            "bench_mobile::test_func",
3228        );
3229        assert!(
3230            result.is_ok(),
3231            "generate_ios_project failed: {:?}",
3232            result.err()
3233        );
3234
3235        // Read Android build.gradle to extract package name
3236        let android_build_gradle = fs::read_to_string(temp_dir.join("android/app/build.gradle"))
3237            .expect("Failed to read Android build.gradle");
3238
3239        // Read iOS project.yml to extract bundle ID prefix
3240        let ios_project_yml = fs::read_to_string(temp_dir.join("ios/BenchRunner/project.yml"))
3241            .expect("Failed to read iOS project.yml");
3242
3243        // Both should use "benchmobile" (without hyphens or underscores)
3244        // Android: namespace = "dev.world.benchmobile"
3245        // iOS: bundleIdPrefix: dev.world.benchmobile
3246        assert!(
3247            android_build_gradle.contains("dev.world.benchmobile"),
3248            "Android package should be 'dev.world.benchmobile', got:\n{}",
3249            android_build_gradle
3250        );
3251        assert!(
3252            ios_project_yml.contains("dev.world.benchmobile"),
3253            "iOS bundle prefix should contain 'dev.world.benchmobile', got:\n{}",
3254            ios_project_yml
3255        );
3256
3257        // Ensure Android doesn't use hyphens or underscores in the package ID component
3258        assert!(
3259            !android_build_gradle.contains("dev.world.bench-mobile"),
3260            "Android package should NOT contain hyphens"
3261        );
3262        assert!(
3263            !android_build_gradle.contains("dev.world.bench_mobile"),
3264            "Android package should NOT contain underscores"
3265        );
3266
3267        // Cleanup
3268        fs::remove_dir_all(&temp_dir).ok();
3269    }
3270
3271    #[test]
3272    fn test_cross_platform_version_consistency() {
3273        // Test that Android and iOS use the same version strings
3274        let temp_dir = env::temp_dir().join("mobench-sdk-version-consistency-test");
3275        let _ = fs::remove_dir_all(&temp_dir);
3276        fs::create_dir_all(&temp_dir).unwrap();
3277
3278        let project_name = "test-project";
3279
3280        // Generate Android project
3281        let result = generate_android_project(&temp_dir, project_name, "test_project::test_func");
3282        assert!(
3283            result.is_ok(),
3284            "generate_android_project failed: {:?}",
3285            result.err()
3286        );
3287
3288        // Generate iOS project
3289        let bundle_id_component = sanitize_bundle_id_component(project_name);
3290        let bundle_prefix = format!("dev.world.{}", bundle_id_component);
3291        let result = generate_ios_project(
3292            &temp_dir,
3293            &project_name.replace('-', "_"),
3294            "BenchRunner",
3295            &bundle_prefix,
3296            "test_project::test_func",
3297        );
3298        assert!(
3299            result.is_ok(),
3300            "generate_ios_project failed: {:?}",
3301            result.err()
3302        );
3303
3304        // Read Android build.gradle
3305        let android_build_gradle = fs::read_to_string(temp_dir.join("android/app/build.gradle"))
3306            .expect("Failed to read Android build.gradle");
3307
3308        // Read iOS project.yml
3309        let ios_project_yml = fs::read_to_string(temp_dir.join("ios/BenchRunner/project.yml"))
3310            .expect("Failed to read iOS project.yml");
3311
3312        // Both should use version "1.0.0"
3313        assert!(
3314            android_build_gradle.contains("versionName \"1.0.0\""),
3315            "Android versionName should be '1.0.0', got:\n{}",
3316            android_build_gradle
3317        );
3318        assert!(
3319            ios_project_yml.contains("CFBundleShortVersionString: \"1.0.0\""),
3320            "iOS CFBundleShortVersionString should be '1.0.0', got:\n{}",
3321            ios_project_yml
3322        );
3323
3324        // Cleanup
3325        fs::remove_dir_all(&temp_dir).ok();
3326    }
3327
3328    #[test]
3329    fn test_bundle_id_prefix_consistency() {
3330        // Test that the bundle ID prefix format is consistent across platforms
3331        let test_cases = vec![
3332            ("my-project", "dev.world.myproject"),
3333            ("bench_mobile", "dev.world.benchmobile"),
3334            ("TestApp", "dev.world.testapp"),
3335            ("app-with-many-dashes", "dev.world.appwithmanydashes"),
3336            (
3337                "app_with_many_underscores",
3338                "dev.world.appwithmanyunderscores",
3339            ),
3340        ];
3341
3342        for (input, expected_prefix) in test_cases {
3343            let sanitized = sanitize_bundle_id_component(input);
3344            let full_prefix = format!("dev.world.{}", sanitized);
3345            assert_eq!(
3346                full_prefix, expected_prefix,
3347                "For input '{}', expected '{}' but got '{}'",
3348                input, expected_prefix, full_prefix
3349            );
3350        }
3351    }
3352}