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    println!("  Generated iOS project at {:?}", output_dir.join("ios"));
1981    println!("  Default benchmark function: {}", default_function);
1982    Ok(())
1983}
1984
1985#[cfg(test)]
1986mod tests {
1987    use super::*;
1988    use std::env;
1989
1990    #[test]
1991    fn test_generate_bench_mobile_crate() {
1992        let temp_dir = env::temp_dir().join("mobench-sdk-test");
1993        fs::create_dir_all(&temp_dir).unwrap();
1994
1995        let result = generate_bench_mobile_crate(&temp_dir, "test_project");
1996        assert!(result.is_ok());
1997
1998        // Verify files were created
1999        assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
2000        assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
2001        assert!(temp_dir.join("bench-mobile/build.rs").exists());
2002        let cargo_toml =
2003            fs::read_to_string(temp_dir.join("bench-mobile/Cargo.toml")).expect("read Cargo.toml");
2004        assert!(
2005            cargo_toml.contains(
2006                r#"mobench-sdk = { path = "..", default-features = false, features = ["registry"] }"#
2007            ),
2008            "generated FFI wrapper should depend on the narrow registry feature, got:\n{cargo_toml}"
2009        );
2010
2011        // Cleanup
2012        fs::remove_dir_all(&temp_dir).ok();
2013    }
2014
2015    #[test]
2016    fn test_generate_android_project_no_unreplaced_placeholders() {
2017        let temp_dir = env::temp_dir().join("mobench-sdk-android-test");
2018        // Clean up any previous test run
2019        let _ = fs::remove_dir_all(&temp_dir);
2020        fs::create_dir_all(&temp_dir).unwrap();
2021
2022        let result =
2023            generate_android_project(&temp_dir, "my-bench-project", "my_bench_project::test_func");
2024        assert!(
2025            result.is_ok(),
2026            "generate_android_project failed: {:?}",
2027            result.err()
2028        );
2029
2030        // Verify key files exist
2031        let android_dir = temp_dir.join("android");
2032        assert!(android_dir.join("settings.gradle").exists());
2033        assert!(android_dir.join("app/build.gradle").exists());
2034        assert!(
2035            android_dir
2036                .join("app/src/main/AndroidManifest.xml")
2037                .exists()
2038        );
2039        assert!(
2040            android_dir
2041                .join("app/src/main/res/values/strings.xml")
2042                .exists()
2043        );
2044        assert!(
2045            android_dir
2046                .join("app/src/main/res/values/themes.xml")
2047                .exists()
2048        );
2049
2050        // Verify no unreplaced placeholders remain in generated files
2051        let files_to_check = [
2052            "settings.gradle",
2053            "app/build.gradle",
2054            "app/src/main/AndroidManifest.xml",
2055            "app/src/main/res/values/strings.xml",
2056            "app/src/main/res/values/themes.xml",
2057        ];
2058
2059        for file in files_to_check {
2060            let path = android_dir.join(file);
2061            let contents =
2062                fs::read_to_string(&path).unwrap_or_else(|_| panic!("Failed to read {}", file));
2063
2064            // Check for unreplaced placeholders
2065            let has_placeholder = contents.contains("{{") && contents.contains("}}");
2066            assert!(
2067                !has_placeholder,
2068                "File {} contains unreplaced template placeholders: {}",
2069                file, contents
2070            );
2071        }
2072
2073        // Verify specific substitutions were made
2074        let settings = fs::read_to_string(android_dir.join("settings.gradle")).unwrap();
2075        assert!(
2076            settings.contains("my-bench-project-android")
2077                || settings.contains("my_bench_project-android"),
2078            "settings.gradle should contain project name"
2079        );
2080
2081        let build_gradle = fs::read_to_string(android_dir.join("app/build.gradle")).unwrap();
2082        // Package name should be sanitized (no hyphens/underscores) for consistency with iOS
2083        assert!(
2084            build_gradle.contains("dev.world.mybenchproject"),
2085            "build.gradle should contain sanitized package name 'dev.world.mybenchproject'"
2086        );
2087        assert!(
2088            !build_gradle.contains("testBuildType \"release\""),
2089            "debug builds should be able to produce assembleDebugAndroidTest"
2090        );
2091        assert!(
2092            build_gradle.contains("mobenchTestBuildType"),
2093            "release builds should be able to request assembleReleaseAndroidTest"
2094        );
2095
2096        let manifest =
2097            fs::read_to_string(android_dir.join("app/src/main/AndroidManifest.xml")).unwrap();
2098        assert!(
2099            manifest.contains("Theme.MyBenchProject"),
2100            "AndroidManifest.xml should contain PascalCase theme name"
2101        );
2102
2103        let strings =
2104            fs::read_to_string(android_dir.join("app/src/main/res/values/strings.xml")).unwrap();
2105        assert!(
2106            strings.contains("Benchmark"),
2107            "strings.xml should contain app name with Benchmark"
2108        );
2109
2110        // Verify Kotlin files are in the correct package directory structure
2111        // For package "dev.world.mybenchproject", files should be in "dev/world/mybenchproject/"
2112        let main_activity_path =
2113            android_dir.join("app/src/main/java/dev/world/mybenchproject/MainActivity.kt");
2114        assert!(
2115            main_activity_path.exists(),
2116            "MainActivity.kt should be in package directory: {:?}",
2117            main_activity_path
2118        );
2119        let main_activity = fs::read_to_string(&main_activity_path).unwrap();
2120        assert!(main_activity.contains("BENCH_RESULT_OK"));
2121        assert!(!main_activity.contains("private const val RESULT_OK"));
2122        assert!(main_activity.contains("fun benchmarkTimeoutSecs()"));
2123        assert!(main_activity.contains("fun heartbeatIntervalSecs()"));
2124        assert!(main_activity.contains("fun checkWorkerExit()"));
2125        assert!(main_activity.contains("fun isBenchmarkFailed()"));
2126        assert!(main_activity.contains("fun getBenchmarkFailureJson()"));
2127        assert!(main_activity.contains("fun emitTimeoutFailureFromTest()"));
2128
2129        let test_activity_path = android_dir
2130            .join("app/src/androidTest/java/dev/world/mybenchproject/MainActivityTest.kt");
2131        assert!(
2132            test_activity_path.exists(),
2133            "MainActivityTest.kt should be in package directory: {:?}",
2134            test_activity_path
2135        );
2136
2137        // Verify the files are NOT in the root java directory
2138        assert!(
2139            !android_dir
2140                .join("app/src/main/java/MainActivity.kt")
2141                .exists(),
2142            "MainActivity.kt should not be in root java directory"
2143        );
2144        assert!(
2145            !android_dir
2146                .join("app/src/androidTest/java/MainActivityTest.kt")
2147                .exists(),
2148            "MainActivityTest.kt should not be in root java directory"
2149        );
2150
2151        // Cleanup
2152        fs::remove_dir_all(&temp_dir).ok();
2153    }
2154
2155    #[test]
2156    fn test_generate_android_project_replaces_previous_package_tree() {
2157        let temp_dir = env::temp_dir().join("mobench-sdk-android-regenerate-test");
2158        let _ = fs::remove_dir_all(&temp_dir);
2159        fs::create_dir_all(&temp_dir).unwrap();
2160
2161        generate_android_project(&temp_dir, "ffi_benchmark", "ffi_benchmark::bench_fibonacci")
2162            .unwrap();
2163        let old_package_dir = temp_dir.join("android/app/src/main/java/dev/world/ffibenchmark");
2164        assert!(
2165            old_package_dir.exists(),
2166            "expected first package tree to exist"
2167        );
2168
2169        generate_android_project(
2170            &temp_dir,
2171            "basic_benchmark",
2172            "basic_benchmark::bench_fibonacci",
2173        )
2174        .unwrap();
2175
2176        let new_package_dir = temp_dir.join("android/app/src/main/java/dev/world/basicbenchmark");
2177        assert!(
2178            new_package_dir.exists(),
2179            "expected new package tree to exist"
2180        );
2181        assert!(
2182            !old_package_dir.exists(),
2183            "old package tree should be removed when regenerating the Android scaffold"
2184        );
2185
2186        fs::remove_dir_all(&temp_dir).ok();
2187    }
2188
2189    #[test]
2190    fn test_generate_android_native_backend_runner_template() {
2191        let temp_dir = env::temp_dir().join("mobench-sdk-android-native-test");
2192        let _ = fs::remove_dir_all(&temp_dir);
2193        fs::create_dir_all(&temp_dir).unwrap();
2194
2195        generate_android_project_with_backend(
2196            &temp_dir,
2197            "native_benchmark",
2198            "native_benchmark::bench_prove",
2199            crate::FfiBackend::NativeCAbi,
2200        )
2201        .unwrap();
2202
2203        let main_activity = fs::read_to_string(
2204            temp_dir.join("android/app/src/main/java/dev/world/nativebenchmark/MainActivity.kt"),
2205        )
2206        .unwrap();
2207        assert!(main_activity.contains("com.sun.jna.Native"));
2208        assert!(main_activity.contains("com.sun.jna.NativeLong"));
2209        assert!(main_activity.contains("mobench_run_benchmark_json"));
2210        assert!(main_activity.contains("specLen: NativeLong"));
2211        assert!(main_activity.contains("@JvmField var len: NativeLong = NativeLong(0)"));
2212        assert!(main_activity.contains("@JvmField var cap: NativeLong = NativeLong(0)"));
2213        assert!(!main_activity.contains("specLen: Long"));
2214        assert!(main_activity.contains("BENCH_JSON"));
2215        assert!(main_activity.contains("bench_spec.json"));
2216        assert!(main_activity.contains("BENCH_RESULT_OK"));
2217        assert!(!main_activity.contains("private const val RESULT_OK"));
2218        assert!(main_activity.contains("fun benchmarkTimeoutSecs()"));
2219        assert!(main_activity.contains("fun heartbeatIntervalSecs()"));
2220        assert!(main_activity.contains("fun checkWorkerExit()"));
2221        assert!(main_activity.contains("fun isBenchmarkFailed()"));
2222        assert!(main_activity.contains("fun getBenchmarkFailureJson()"));
2223        assert!(main_activity.contains("fun emitTimeoutFailureFromTest()"));
2224        assert!(
2225            !main_activity.contains("uniffi."),
2226            "native Android runner must not import UniFFI bindings:\n{}",
2227            main_activity
2228        );
2229        assert!(
2230            !main_activity.contains("runBenchmark("),
2231            "native Android runner must call the JSON C ABI, not UniFFI runBenchmark"
2232        );
2233
2234        fs::remove_dir_all(&temp_dir).ok();
2235    }
2236
2237    #[test]
2238    fn test_generate_android_boltffi_backend_runner_template() {
2239        let temp_dir = env::temp_dir().join("mobench-sdk-android-boltffi-test");
2240        let _ = fs::remove_dir_all(&temp_dir);
2241        fs::create_dir_all(&temp_dir).unwrap();
2242
2243        generate_android_project_with_backend(
2244            &temp_dir,
2245            "bolt_benchmark",
2246            "bolt_benchmark::bench_prove",
2247            crate::FfiBackend::BoltFfi,
2248        )
2249        .unwrap();
2250
2251        let main_activity = fs::read_to_string(
2252            temp_dir.join("android/app/src/main/java/dev/world/boltbenchmark/MainActivity.kt"),
2253        )
2254        .unwrap();
2255        assert!(main_activity.contains("import com.mobench.boltbenchmark.runBenchmarkJson"));
2256        assert!(main_activity.contains("runBenchmarkJson(specJson = spec.toString())"));
2257        assert!(main_activity.contains("fun isBenchmarkComplete()"));
2258        assert!(main_activity.contains("fun benchmarkTimeoutSecs()"));
2259        assert!(main_activity.contains("fun heartbeatIntervalSecs()"));
2260        assert!(main_activity.contains("fun checkWorkerExit()"));
2261        assert!(main_activity.contains("fun isBenchmarkFailed()"));
2262        assert!(main_activity.contains("fun getBenchmarkFailureJson()"));
2263        assert!(main_activity.contains("fun emitTimeoutFailureFromTest()"));
2264        assert!(main_activity.contains("BENCH_JSON"));
2265        assert!(
2266            !main_activity.contains("uniffi."),
2267            "BoltFFI Android runner must not import UniFFI bindings:\n{}",
2268            main_activity
2269        );
2270        assert!(
2271            !main_activity.contains("com.sun.jna"),
2272            "BoltFFI Android runner must not use the native C ABI JNA bridge"
2273        );
2274
2275        let boltffi_toml = fs::read_to_string(temp_dir.join("boltffi.toml")).unwrap();
2276        assert!(boltffi_toml.contains("name = \"bolt_benchmark\""));
2277        assert!(boltffi_toml.contains("crate = \"bolt_benchmark\""));
2278        assert!(boltffi_toml.contains("package = \"com.mobench.boltbenchmark\""));
2279        assert!(boltffi_toml.contains("desktop_loader = \"system\""));
2280        assert!(boltffi_toml.contains("output = \"target/mobench/android/app/src/main/java\""));
2281
2282        fs::remove_dir_all(&temp_dir).ok();
2283    }
2284
2285    #[test]
2286    fn test_ensure_android_project_regenerates_when_ffi_backend_changes() {
2287        let temp_dir = env::temp_dir().join("mobench-sdk-android-backend-switch-test");
2288        let _ = fs::remove_dir_all(&temp_dir);
2289        fs::create_dir_all(&temp_dir).unwrap();
2290
2291        generate_android_project_with_backend(
2292            &temp_dir,
2293            "switch_benchmark",
2294            "switch_benchmark::bench_prove",
2295            crate::FfiBackend::Uniffi,
2296        )
2297        .unwrap();
2298
2299        let main_activity_path =
2300            temp_dir.join("android/app/src/main/java/dev/world/switchbenchmark/MainActivity.kt");
2301        let uniffi_main = fs::read_to_string(&main_activity_path).unwrap();
2302        assert!(uniffi_main.contains("uniffi."));
2303
2304        ensure_android_project_with_backend_options(
2305            &temp_dir,
2306            "switch_benchmark",
2307            None,
2308            None,
2309            crate::FfiBackend::BoltFfi,
2310        )
2311        .unwrap();
2312
2313        let boltffi_main = fs::read_to_string(&main_activity_path).unwrap();
2314        assert!(boltffi_main.contains("runBenchmarkJson(specJson = spec.toString())"));
2315        assert!(
2316            !boltffi_main.contains("uniffi."),
2317            "backend changes should regenerate the Android runner:\n{}",
2318            boltffi_main
2319        );
2320
2321        fs::remove_dir_all(&temp_dir).ok();
2322    }
2323
2324    #[test]
2325    fn test_ensure_android_project_refreshes_existing_backend_scaffolding() {
2326        let temp_dir = env::temp_dir().join("mobench-sdk-android-refresh-test");
2327        let _ = fs::remove_dir_all(&temp_dir);
2328        fs::create_dir_all(&temp_dir).unwrap();
2329
2330        generate_android_project_with_backend(
2331            &temp_dir,
2332            "refresh_benchmark",
2333            "refresh_benchmark::bench_prove",
2334            crate::FfiBackend::BoltFfi,
2335        )
2336        .unwrap();
2337
2338        let main_activity_path =
2339            temp_dir.join("android/app/src/main/java/dev/world/refreshbenchmark/MainActivity.kt");
2340        let stale_main = fs::read_to_string(&main_activity_path)
2341            .unwrap()
2342            .replace("BENCH_JSON_START", "STALE_BENCH_JSON_START");
2343        fs::write(&main_activity_path, stale_main).unwrap();
2344
2345        ensure_android_project_with_backend_options(
2346            &temp_dir,
2347            "refresh_benchmark",
2348            None,
2349            None,
2350            crate::FfiBackend::BoltFfi,
2351        )
2352        .unwrap();
2353
2354        let refreshed_main = fs::read_to_string(&main_activity_path).unwrap();
2355        assert!(refreshed_main.contains("BENCH_JSON_START"));
2356        assert!(
2357            !refreshed_main.contains("STALE_BENCH_JSON_START"),
2358            "existing same-backend scaffolding should be refreshed:\n{}",
2359            refreshed_main
2360        );
2361
2362        fs::remove_dir_all(&temp_dir).ok();
2363    }
2364
2365    #[test]
2366    fn test_write_boltffi_config_can_target_explicit_output_dir() {
2367        let temp_dir = env::temp_dir().join("mobench-sdk-boltffi-config-output-test");
2368        let _ = fs::remove_dir_all(&temp_dir);
2369        fs::create_dir_all(&temp_dir).unwrap();
2370        let output_dir = temp_dir.join("custom-mobench-output");
2371
2372        write_boltffi_config_with_output_dir(
2373            &temp_dir,
2374            "bolt_benchmark",
2375            "bolt-benchmark",
2376            "BenchRunner",
2377            "com.mobench.boltbenchmark",
2378            &["arm64".to_string(), "x86_64".to_string()],
2379            &output_dir,
2380        )
2381        .unwrap();
2382
2383        let boltffi_toml = fs::read_to_string(temp_dir.join("boltffi.toml")).unwrap();
2384        assert!(boltffi_toml.contains(&format!(
2385            "output = \"{}\"",
2386            output_dir
2387                .join("android/app/src/main/java")
2388                .to_string_lossy()
2389        )));
2390        assert!(boltffi_toml.contains(&format!(
2391            "output = \"{}\"",
2392            output_dir
2393                .join("ios/BenchRunner/BenchRunner/Generated/BoltFFIGenerated")
2394                .to_string_lossy()
2395        )));
2396
2397        fs::remove_dir_all(&temp_dir).ok();
2398    }
2399
2400    #[test]
2401    fn test_is_template_file() {
2402        assert!(is_template_file(Path::new("settings.gradle")));
2403        assert!(is_template_file(Path::new("app/build.gradle")));
2404        assert!(is_template_file(Path::new("AndroidManifest.xml")));
2405        assert!(is_template_file(Path::new("strings.xml")));
2406        assert!(is_template_file(Path::new("MainActivity.kt.template")));
2407        assert!(is_template_file(Path::new("project.yml")));
2408        assert!(is_template_file(Path::new("Info.plist")));
2409        assert!(!is_template_file(Path::new("libfoo.so")));
2410        assert!(!is_template_file(Path::new("image.png")));
2411    }
2412
2413    #[test]
2414    fn test_mobile_templates_read_process_peak_memory_compatibly() {
2415        let android =
2416            include_str!("../templates/android/app/src/main/java/MainActivity.kt.template");
2417        assert!(
2418            !android.contains("sample.processPeakMemoryKb"),
2419            "Android template should not require generated bindings to expose processPeakMemoryKb"
2420        );
2421        assert!(
2422            !android.contains("it.processPeakMemoryKb"),
2423            "Android template should not require generated bindings to expose processPeakMemoryKb"
2424        );
2425        assert!(android.contains("optionalProcessPeakMemoryKb(sample)"));
2426        assert!(
2427            !android.contains("sample.cpuTimeMs"),
2428            "Android template should tolerate BenchSample without cpuTimeMs"
2429        );
2430        assert!(
2431            !android.contains("sample.peakMemoryKb"),
2432            "Android template should tolerate BenchSample without peakMemoryKb"
2433        );
2434        assert!(
2435            !android.contains("report.phases"),
2436            "Android template should tolerate BenchReport without phases"
2437        );
2438        assert!(android.contains("ProcessMemorySampler"));
2439        assert!(android.contains("sampleIntervalMs: Long = 1000L"));
2440        assert!(android.contains("/proc/self/smaps_rollup"));
2441        assert!(android.contains("class BenchmarkWorkerService : Service()"));
2442        assert!(android.contains("ResultReceiver(Handler(Looper.getMainLooper()))"));
2443        assert!(android.contains("startForegroundService(intent)"));
2444        assert!(android.contains("startForeground(FOREGROUND_NOTIFICATION_ID"));
2445        assert!(android.contains("fun isBenchmarkComplete()"));
2446        assert!(!android.contains("resultLatch.await"));
2447        assert!(android.contains("memory_process\", \"isolated_worker\""));
2448
2449        let android_test = include_str!(
2450            "../templates/android/app/src/androidTest/java/MainActivityTest.kt.template"
2451        );
2452        assert!(android_test.contains("Log.i(\"BenchRunnerTest\""));
2453        assert!(android_test.contains("Thread.sleep(pollMs)"));
2454        assert!(
2455            android_test.contains("TimeUnit.SECONDS.toMillis({{ANDROID_HEARTBEAT_INTERVAL_SECS}})")
2456        );
2457        assert!(
2458            android_test.contains("TimeUnit.SECONDS.toMillis({{ANDROID_BENCHMARK_TIMEOUT_SECS}})")
2459        );
2460        assert!(android_test.contains("activity.isBenchmarkComplete()"));
2461
2462        let ios_test = include_str!(
2463            "../templates/ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift.template"
2464        );
2465        assert!(
2466            ios_test.contains("XCTAssertNil(payload[\"error\"]"),
2467            "iOS XCUITest template should fail when the benchmark report is an error payload"
2468        );
2469        assert!(ios_test.contains("JSONSerialization.jsonObject"));
2470        assert!(ios_test.contains("reportedFunction"));
2471        assert!(ios_test.contains("payload[\"samples_ns\"]"));
2472        assert!(ios_test.contains("payload[\"schema_version\"]"));
2473
2474        let android_manifest =
2475            include_str!("../templates/android/app/src/main/AndroidManifest.xml");
2476        assert!(android_manifest.contains("android.permission.FOREGROUND_SERVICE"));
2477        assert!(android_manifest.contains("android.permission.FOREGROUND_SERVICE_DATA_SYNC"));
2478        assert!(android_manifest.contains("android:name=\".BenchmarkWorkerService\""));
2479        assert!(android_manifest.contains("android:foregroundServiceType=\"dataSync\""));
2480        assert!(android_manifest.contains("android:process=\":mobench_worker\""));
2481
2482        let android_build_gradle = include_str!("../templates/android/app/build.gradle");
2483        assert!(android_build_gradle.contains("generatedMainBenchSpec"));
2484        assert!(android_build_gradle.contains("if (!generatedMainBenchSpec.exists())"));
2485
2486        let ios =
2487            include_str!("../templates/ios/BenchRunner/BenchRunner/BenchRunnerFFI.swift.template");
2488        assert!(
2489            !ios.contains("sample.processPeakMemoryKb"),
2490            "iOS template should not require generated bindings to expose processPeakMemoryKb"
2491        );
2492        assert!(
2493            !ios.contains(r"\.processPeakMemoryKb"),
2494            "iOS template should not require generated bindings to expose processPeakMemoryKb"
2495        );
2496        assert!(ios.contains("optionalProcessPeakMemoryKb(sample)"));
2497        assert!(ios.contains("return [\n                \"name\": name,"));
2498        assert!(
2499            !ios.contains("sample.cpuTimeMs"),
2500            "iOS template should tolerate BenchSample without cpuTimeMs"
2501        );
2502        assert!(
2503            !ios.contains("sample.peakMemoryKb"),
2504            "iOS template should tolerate BenchSample without peakMemoryKb"
2505        );
2506        assert!(
2507            !ios.contains("report.phases"),
2508            "iOS template should tolerate BenchReport without phases"
2509        );
2510        assert!(ios.contains("compactMap { optionalProcessPeakMemoryKb($0) }"));
2511        assert!(ios.contains("ProcessMemorySampler"));
2512        assert!(ios.contains("currentProcessResidentMemoryKb"));
2513        assert!(ios.contains("task_info("));
2514        assert!(ios.contains("\"memory_process\": \"benchmark_app\""));
2515        assert!(ios.contains("params: params,"));
2516        assert!(ios.contains("processPeakSamplesKb.max() ?? runProcessPeakMemoryKb"));
2517    }
2518
2519    #[test]
2520    fn all_generated_runner_backends_emit_strict_v2_identity_and_counts() {
2521        let templates = [
2522            include_str!("../templates/android/app/src/main/java/MainActivity.kt.template"),
2523            include_str!("../templates/ios/BenchRunner/BenchRunner/BenchRunnerFFI.swift.template"),
2524            include_str!("native_templates/android/MainActivity.kt.template"),
2525            include_str!("native_templates/ios/BenchRunnerFFI.swift.template"),
2526            include_str!("boltffi_templates/android/MainActivity.kt.template"),
2527            include_str!("boltffi_templates/ios/BenchRunnerFFI.swift.template"),
2528        ];
2529
2530        for template in templates {
2531            for field in [
2532                "mobench.run/v2",
2533                "run_id",
2534                "nonce",
2535                "logical_session_id",
2536                "function_id",
2537                "producer",
2538                "requested",
2539                "observed",
2540                "samples_ns",
2541                "outcome",
2542                "success",
2543                "code",
2544            ] {
2545                assert!(
2546                    template.contains(field),
2547                    "generated runner omitted strict v2 field `{field}`"
2548                );
2549            }
2550        }
2551    }
2552
2553    #[test]
2554    fn android_worker_failures_are_detected_and_v2_bound() {
2555        let uniffi =
2556            include_str!("../templates/android/app/src/main/java/MainActivity.kt.template");
2557        let native = include_str!("native_templates/android/MainActivity.kt.template");
2558
2559        for template in [uniffi, native] {
2560            for contract in [
2561                "BENCH_RESULT_HEARTBEAT",
2562                "WorkerHeartbeat",
2563                "BENCH_HEARTBEAT_JSON",
2564                "workerPid",
2565                "runningAppProcesses()",
2566                "worker_exit",
2567                "RESULT_FAILURE_JSON_EXTRA",
2568                "androidExitInfoJson",
2569                "emitFailure(\"timeout\"",
2570                "applyV2Envelope",
2571                "\"failure\"",
2572            ] {
2573                assert!(
2574                    template.contains(contract),
2575                    "Android worker template omitted fail-closed contract `{contract}`"
2576                );
2577            }
2578            for watchdog_contract in [
2579                "watchdogHandler.postDelayed(watchdog",
2580                "override fun onDestroy()",
2581                "stopWatchdog()",
2582                "resultText?.text = message",
2583                "elapsedMs >= params.timeoutSecs * 1_000L",
2584            ] {
2585                assert!(
2586                    template.contains(watchdog_contract),
2587                    "Android worker template omitted watchdog contract `{watchdog_contract}`"
2588                );
2589            }
2590        }
2591        assert!(
2592            !uniffi.contains("benchmarkComplete || workerPid == null"),
2593            "the watchdog must detect a worker killed before its first heartbeat"
2594        );
2595
2596        let boltffi = include_str!("boltffi_templates/android/MainActivity.kt.template");
2597        assert!(boltffi.contains("emitFailure(\"exception\""));
2598        assert!(boltffi.contains("applyV2Envelope"));
2599        assert!(boltffi.contains("\"failure\""));
2600    }
2601
2602    #[test]
2603    fn ios_result_transport_has_heartbeat_and_redundant_accessibility_channels() {
2604        let swiftui =
2605            include_str!("../templates/ios/BenchRunner/BenchRunner/ContentView.swift.template");
2606        let uikit = include_str!(
2607            "../templates/ios/BenchRunner/BenchRunner/UIKitLegacyRunner.swift.template"
2608        );
2609        let ui_test = include_str!(
2610            "../templates/ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift.template"
2611        );
2612
2613        for template in [swiftui, uikit] {
2614            assert!(template.contains("benchmarkHeartbeat"));
2615            assert!(template.contains("MOBENCH_HEARTBEAT app interaction"));
2616            assert!(template.contains("benchmarkReportJSON"));
2617            assert!(template.contains("accessibilityLabel"));
2618            assert!(template.contains("accessibilityValue"));
2619        }
2620        assert!(ui_test.contains("waitForBenchmarkCompletion"));
2621        assert!(ui_test.contains("MOBENCH_HEARTBEAT waiting for benchmark completion"));
2622        assert!(ui_test.contains("app.buttons[\"benchmarkHeartbeat\"]"));
2623        assert!(ui_test.contains("app.activate()"));
2624        assert!(ui_test.contains("firstValidJSON([reportValue, reportElement.label])"));
2625        assert!(ui_test.contains("validateBenchmarkReport(jsonString)"));
2626    }
2627
2628    #[test]
2629    fn editable_native_runner_templates_match_embedded_sources() {
2630        assert_eq!(
2631            include_str!("../templates/android/app/src/main/java/MainActivity.kt.template"),
2632            include_str!("../../../templates/android/app/src/main/java/MainActivity.kt.template"),
2633        );
2634        assert_eq!(
2635            include_str!(
2636                "../templates/android/app/src/androidTest/java/MainActivityTest.kt.template"
2637            ),
2638            include_str!(
2639                "../../../templates/android/app/src/androidTest/java/MainActivityTest.kt.template"
2640            ),
2641        );
2642        assert_eq!(
2643            include_str!("../templates/ios/BenchRunner/BenchRunner/ContentView.swift.template"),
2644            include_str!(
2645                "../../../templates/ios/BenchRunner/BenchRunner/ContentView.swift.template"
2646            ),
2647        );
2648        assert_eq!(
2649            include_str!(
2650                "../templates/ios/BenchRunner/BenchRunner/UIKitLegacyRunner.swift.template"
2651            ),
2652            include_str!(
2653                "../../../templates/ios/BenchRunner/BenchRunner/UIKitLegacyRunner.swift.template"
2654            ),
2655        );
2656        assert_eq!(
2657            include_str!(
2658                "../templates/ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift.template"
2659            ),
2660            include_str!(
2661                "../../../templates/ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift.template"
2662            ),
2663        );
2664    }
2665
2666    #[test]
2667    fn generated_android_test_uses_configured_timeout_and_heartbeat() {
2668        let temp_dir = env::temp_dir().join("mobench-sdk-android-timeout-test");
2669        let _ = fs::remove_dir_all(&temp_dir);
2670        fs::create_dir_all(&temp_dir).expect("create temp dir");
2671
2672        unsafe {
2673            std::env::set_var("MOBENCH_ANDROID_BENCHMARK_TIMEOUT_SECS", "7200");
2674            std::env::set_var("MOBENCH_ANDROID_HEARTBEAT_INTERVAL_SECS", "15");
2675        }
2676        let result = generate_android_project_with_backend(
2677            &temp_dir,
2678            "my-bench-project",
2679            "sample_fns::fibonacci",
2680            crate::FfiBackend::NativeCAbi,
2681        );
2682        unsafe {
2683            std::env::remove_var("MOBENCH_ANDROID_BENCHMARK_TIMEOUT_SECS");
2684            std::env::remove_var("MOBENCH_ANDROID_HEARTBEAT_INTERVAL_SECS");
2685        }
2686        result.expect("generate Android project");
2687
2688        let android_test =
2689            fs::read_to_string(temp_dir.join(
2690                "android/app/src/androidTest/java/dev/world/mybenchproject/MainActivityTest.kt",
2691            ))
2692            .expect("read MainActivityTest.kt");
2693
2694        assert!(android_test.contains("TimeUnit.SECONDS.toMillis(7200)"));
2695        assert!(android_test.contains("TimeUnit.SECONDS.toMillis(15)"));
2696        let _ = fs::remove_dir_all(&temp_dir);
2697    }
2698
2699    #[test]
2700    fn test_validate_no_unreplaced_placeholders() {
2701        // Should pass with no placeholders
2702        assert!(validate_no_unreplaced_placeholders("hello world", Path::new("test.txt")).is_ok());
2703
2704        // Should pass with Gradle variables (not our placeholders)
2705        assert!(validate_no_unreplaced_placeholders("${ENV_VAR}", Path::new("test.txt")).is_ok());
2706
2707        // Should fail with unreplaced template placeholders
2708        let result = validate_no_unreplaced_placeholders("hello {{NAME}}", Path::new("test.txt"));
2709        assert!(result.is_err());
2710        let err = result.unwrap_err().to_string();
2711        assert!(err.contains("{{NAME}}"));
2712    }
2713
2714    #[test]
2715    fn test_to_pascal_case() {
2716        assert_eq!(to_pascal_case("my-project"), "MyProject");
2717        assert_eq!(to_pascal_case("my_project"), "MyProject");
2718        assert_eq!(to_pascal_case("myproject"), "Myproject");
2719        assert_eq!(to_pascal_case("my-bench-project"), "MyBenchProject");
2720    }
2721
2722    #[test]
2723    fn test_detect_default_function_finds_benchmark() {
2724        let temp_dir = env::temp_dir().join("mobench-sdk-detect-test");
2725        let _ = fs::remove_dir_all(&temp_dir);
2726        fs::create_dir_all(temp_dir.join("src")).unwrap();
2727
2728        // Create a lib.rs with a benchmark function
2729        let lib_content = r#"
2730use mobench_sdk::benchmark;
2731
2732/// Some docs
2733#[benchmark]
2734fn my_benchmark_func() {
2735    // benchmark code
2736}
2737
2738fn helper_func() {}
2739"#;
2740        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
2741        fs::write(temp_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
2742
2743        let result = detect_default_function(&temp_dir, "my_crate");
2744        assert_eq!(result, Some("my_crate::my_benchmark_func".to_string()));
2745
2746        // Cleanup
2747        fs::remove_dir_all(&temp_dir).ok();
2748    }
2749
2750    #[test]
2751    fn test_detect_default_function_no_benchmark() {
2752        let temp_dir = env::temp_dir().join("mobench-sdk-detect-none-test");
2753        let _ = fs::remove_dir_all(&temp_dir);
2754        fs::create_dir_all(temp_dir.join("src")).unwrap();
2755
2756        // Create a lib.rs without benchmark functions
2757        let lib_content = r#"
2758fn regular_function() {
2759    // no benchmark here
2760}
2761"#;
2762        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
2763
2764        let result = detect_default_function(&temp_dir, "my_crate");
2765        assert!(result.is_none());
2766
2767        // Cleanup
2768        fs::remove_dir_all(&temp_dir).ok();
2769    }
2770
2771    #[test]
2772    fn test_detect_default_function_pub_fn() {
2773        let temp_dir = env::temp_dir().join("mobench-sdk-detect-pub-test");
2774        let _ = fs::remove_dir_all(&temp_dir);
2775        fs::create_dir_all(temp_dir.join("src")).unwrap();
2776
2777        // Create a lib.rs with a public benchmark function
2778        let lib_content = r#"
2779#[benchmark]
2780pub fn public_bench() {
2781    // benchmark code
2782}
2783"#;
2784        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
2785
2786        let result = detect_default_function(&temp_dir, "test-crate");
2787        assert_eq!(result, Some("test_crate::public_bench".to_string()));
2788
2789        // Cleanup
2790        fs::remove_dir_all(&temp_dir).ok();
2791    }
2792
2793    #[test]
2794    fn test_resolve_default_function_fallback() {
2795        let temp_dir = env::temp_dir().join("mobench-sdk-resolve-test");
2796        let _ = fs::remove_dir_all(&temp_dir);
2797        fs::create_dir_all(&temp_dir).unwrap();
2798
2799        // No lib.rs exists, should fall back to default
2800        let result = resolve_default_function(&temp_dir, "my-crate", None);
2801        assert_eq!(result, "my_crate::example_benchmark");
2802
2803        // Cleanup
2804        fs::remove_dir_all(&temp_dir).ok();
2805    }
2806
2807    #[test]
2808    fn test_sanitize_bundle_id_component() {
2809        // Hyphens should be removed
2810        assert_eq!(sanitize_bundle_id_component("bench-mobile"), "benchmobile");
2811        // Underscores should be removed
2812        assert_eq!(sanitize_bundle_id_component("bench_mobile"), "benchmobile");
2813        // Mixed separators should all be removed
2814        assert_eq!(
2815            sanitize_bundle_id_component("my-project_name"),
2816            "myprojectname"
2817        );
2818        // Already valid should remain unchanged (but lowercase)
2819        assert_eq!(sanitize_bundle_id_component("benchmobile"), "benchmobile");
2820        // Numbers should be preserved
2821        assert_eq!(sanitize_bundle_id_component("bench2mobile"), "bench2mobile");
2822        // Uppercase should be lowercased
2823        assert_eq!(sanitize_bundle_id_component("BenchMobile"), "benchmobile");
2824        // Complex case
2825        assert_eq!(
2826            sanitize_bundle_id_component("My-Complex_Project-123"),
2827            "mycomplexproject123"
2828        );
2829    }
2830
2831    #[test]
2832    fn test_generate_ios_project_bundle_id_not_duplicated() {
2833        let temp_dir = env::temp_dir().join("mobench-sdk-ios-bundle-test");
2834        // Clean up any previous test run
2835        let _ = fs::remove_dir_all(&temp_dir);
2836        fs::create_dir_all(&temp_dir).unwrap();
2837
2838        // Use a crate name that would previously cause duplication
2839        let crate_name = "bench-mobile";
2840        let bundle_prefix = "dev.world.benchmobile";
2841        let project_pascal = "BenchRunner";
2842
2843        let result = generate_ios_project(
2844            &temp_dir,
2845            crate_name,
2846            project_pascal,
2847            bundle_prefix,
2848            "bench_mobile::test_func",
2849        );
2850        assert!(
2851            result.is_ok(),
2852            "generate_ios_project failed: {:?}",
2853            result.err()
2854        );
2855
2856        // Verify project.yml was created
2857        let project_yml_path = temp_dir.join("ios/BenchRunner/project.yml");
2858        assert!(project_yml_path.exists(), "project.yml should exist");
2859
2860        // Read and verify the bundle ID is correct (not duplicated)
2861        let project_yml = fs::read_to_string(&project_yml_path).unwrap();
2862
2863        // The bundle ID should be "dev.world.benchmobile.BenchRunner"
2864        // NOT "dev.world.benchmobile.benchmobile"
2865        assert!(
2866            project_yml.contains("dev.world.benchmobile.BenchRunner"),
2867            "Bundle ID should be 'dev.world.benchmobile.BenchRunner', got:\n{}",
2868            project_yml
2869        );
2870        assert!(
2871            !project_yml.contains("dev.world.benchmobile.benchmobile"),
2872            "Bundle ID should NOT be duplicated as 'dev.world.benchmobile.benchmobile', got:\n{}",
2873            project_yml
2874        );
2875        assert!(
2876            project_yml.contains("embed: false"),
2877            "Static xcframework dependency should be link-only, got:\n{}",
2878            project_yml
2879        );
2880
2881        // Cleanup
2882        fs::remove_dir_all(&temp_dir).ok();
2883    }
2884
2885    #[test]
2886    fn test_generate_ios_project_preserves_existing_resources_on_regeneration() {
2887        let temp_dir = env::temp_dir().join("mobench-sdk-ios-resources-regenerate-test");
2888        let _ = fs::remove_dir_all(&temp_dir);
2889        fs::create_dir_all(&temp_dir).unwrap();
2890
2891        generate_ios_project(
2892            &temp_dir,
2893            "bench_mobile",
2894            "BenchRunner",
2895            "dev.world.benchmobile",
2896            "bench_mobile::bench_prepare",
2897        )
2898        .unwrap();
2899
2900        let resources_dir = temp_dir.join("ios/BenchRunner/BenchRunner/Resources");
2901        fs::create_dir_all(resources_dir.join("nested")).unwrap();
2902        fs::write(
2903            resources_dir.join("bench_spec.json"),
2904            r#"{"function":"bench_mobile::bench_prove","iterations":2,"warmup":1}"#,
2905        )
2906        .unwrap();
2907        fs::write(
2908            resources_dir.join("bench_meta.json"),
2909            r#"{"build_id":"build-123"}"#,
2910        )
2911        .unwrap();
2912        fs::write(resources_dir.join("nested/custom.txt"), "keep me").unwrap();
2913
2914        generate_ios_project(
2915            &temp_dir,
2916            "bench_mobile",
2917            "BenchRunner",
2918            "dev.world.benchmobile",
2919            "bench_mobile::bench_prepare",
2920        )
2921        .unwrap();
2922
2923        assert_eq!(
2924            fs::read_to_string(resources_dir.join("bench_spec.json")).unwrap(),
2925            r#"{"function":"bench_mobile::bench_prove","iterations":2,"warmup":1}"#
2926        );
2927        assert_eq!(
2928            fs::read_to_string(resources_dir.join("bench_meta.json")).unwrap(),
2929            r#"{"build_id":"build-123"}"#
2930        );
2931        assert_eq!(
2932            fs::read_to_string(resources_dir.join("nested/custom.txt")).unwrap(),
2933            "keep me"
2934        );
2935
2936        fs::remove_dir_all(&temp_dir).ok();
2937    }
2938
2939    #[test]
2940    fn test_generate_ios_native_backend_runner_template() {
2941        let temp_dir = env::temp_dir().join("mobench-sdk-ios-native-test");
2942        let _ = fs::remove_dir_all(&temp_dir);
2943        fs::create_dir_all(&temp_dir).unwrap();
2944
2945        generate_ios_project_with_backend(
2946            &temp_dir,
2947            "native_benchmark",
2948            "BenchRunner",
2949            "dev.world.nativebenchmark",
2950            "native_benchmark::bench_prove",
2951            crate::FfiBackend::NativeCAbi,
2952        )
2953        .unwrap();
2954
2955        let ffi =
2956            fs::read_to_string(temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunnerFFI.swift"))
2957                .unwrap();
2958        assert!(ffi.contains("mobench_run_benchmark_json"));
2959        assert!(ffi.contains("mobench_free_buf"));
2960        assert!(ffi.contains("BENCH_FUNCTION"));
2961        assert!(
2962            !ffi.contains("runBenchmark(spec:"),
2963            "native iOS runner must call the JSON C ABI, not UniFFI runBenchmark"
2964        );
2965        assert!(
2966            !ffi.contains("let report: BenchReport"),
2967            "native iOS runner should operate on JSON, not UniFFI BenchReport"
2968        );
2969
2970        let header = fs::read_to_string(
2971            temp_dir.join("ios/BenchRunner/BenchRunner/Generated/native_benchmarkFFI.h"),
2972        )
2973        .unwrap();
2974        assert!(header.contains("mobench_run_benchmark_json"));
2975        let bridging_header = fs::read_to_string(
2976            temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunner-Bridging-Header.h"),
2977        )
2978        .unwrap();
2979        assert!(bridging_header.contains("#import \"native_benchmarkFFI.h\""));
2980
2981        fs::remove_dir_all(&temp_dir).ok();
2982    }
2983
2984    #[test]
2985    fn test_generate_ios_boltffi_backend_runner_template() {
2986        let temp_dir = env::temp_dir().join("mobench-sdk-ios-boltffi-test");
2987        let _ = fs::remove_dir_all(&temp_dir);
2988        fs::create_dir_all(&temp_dir).unwrap();
2989
2990        generate_ios_project_with_backend(
2991            &temp_dir,
2992            "bolt_benchmark",
2993            "BenchRunner",
2994            "dev.world.boltbenchmark",
2995            "bolt_benchmark::bench_prove",
2996            crate::FfiBackend::BoltFfi,
2997        )
2998        .unwrap();
2999
3000        let ffi =
3001            fs::read_to_string(temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunnerFFI.swift"))
3002                .unwrap();
3003        assert!(ffi.contains("runBenchmarkJson(specJson:"));
3004        assert!(ffi.contains("BENCH_FUNCTION"));
3005        assert!(
3006            !ffi.contains("runBenchmark(spec:"),
3007            "BoltFFI iOS runner must call BoltFFI JSON bindings, not UniFFI runBenchmark"
3008        );
3009        assert!(
3010            !ffi.contains("mobench_run_benchmark_json"),
3011            "BoltFFI iOS runner must not call the native C ABI bridge"
3012        );
3013        let bridging_header = fs::read_to_string(
3014            temp_dir.join("ios/BenchRunner/BenchRunner/BenchRunner-Bridging-Header.h"),
3015        )
3016        .unwrap();
3017        assert!(
3018            !bridging_header.contains("#import"),
3019            "BoltFFI iOS runner should import the generated C module from Swift, not a UniFFI-style bridging header"
3020        );
3021
3022        let boltffi_toml = fs::read_to_string(temp_dir.join("boltffi.toml")).unwrap();
3023        assert!(boltffi_toml.contains("module_name = \"BenchRunner\""));
3024        assert!(boltffi_toml.contains("ffi_module_name = \"bolt_benchmarkFFI\""));
3025        assert!(boltffi_toml.contains(
3026            "output = \"target/mobench/ios/BenchRunner/BenchRunner/Generated/BoltFFIGenerated\""
3027        ));
3028
3029        fs::remove_dir_all(&temp_dir).ok();
3030    }
3031
3032    #[test]
3033    fn test_ensure_ios_project_refreshes_existing_content_view_template() {
3034        let temp_dir = env::temp_dir().join("mobench-sdk-ios-refresh-test");
3035        let _ = fs::remove_dir_all(&temp_dir);
3036        fs::create_dir_all(&temp_dir).unwrap();
3037
3038        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3039            .expect("initial iOS project generation should succeed");
3040
3041        let content_view_path = temp_dir.join("ios/BenchRunner/BenchRunner/ContentView.swift");
3042        assert!(content_view_path.exists(), "ContentView.swift should exist");
3043
3044        fs::write(&content_view_path, "stale generated content").unwrap();
3045
3046        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3047            .expect("refreshing existing iOS project should succeed");
3048
3049        let refreshed = fs::read_to_string(&content_view_path).unwrap();
3050        assert!(
3051            refreshed.contains("ProfileLaunchOptions"),
3052            "refreshed ContentView.swift should contain the latest profiling template, got:\n{}",
3053            refreshed
3054        );
3055        assert!(
3056            refreshed.contains("repeatUntilMs"),
3057            "refreshed ContentView.swift should contain repeat-until profiling support, got:\n{}",
3058            refreshed
3059        );
3060        assert!(
3061            refreshed.contains("Task.detached(priority: .userInitiated)"),
3062            "refreshed ContentView.swift should run benchmarks off the main actor, got:\n{}",
3063            refreshed
3064        );
3065        assert!(
3066            refreshed.contains("await MainActor.run"),
3067            "refreshed ContentView.swift should apply UI updates on the main actor, got:\n{}",
3068            refreshed
3069        );
3070
3071        fs::remove_dir_all(&temp_dir).ok();
3072    }
3073
3074    #[test]
3075    fn test_ensure_ios_project_refreshes_existing_ui_test_timeout_template() {
3076        let temp_dir = env::temp_dir().join("mobench-sdk-ios-uitest-refresh-test");
3077        let _ = fs::remove_dir_all(&temp_dir);
3078        fs::create_dir_all(&temp_dir).unwrap();
3079
3080        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3081            .expect("initial iOS project generation should succeed");
3082
3083        let ui_test_path =
3084            temp_dir.join("ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift");
3085        assert!(
3086            ui_test_path.exists(),
3087            "BenchRunnerUITests.swift should exist"
3088        );
3089
3090        fs::write(&ui_test_path, "stale generated content").unwrap();
3091
3092        ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
3093            .expect("refreshing existing iOS project should succeed");
3094
3095        let refreshed = fs::read_to_string(&ui_test_path).unwrap();
3096        assert!(
3097            refreshed.contains("private let defaultBenchmarkTimeout: TimeInterval = 300.0"),
3098            "refreshed BenchRunnerUITests.swift should include the default timeout, got:\n{}",
3099            refreshed
3100        );
3101        assert!(
3102            refreshed.contains(
3103                "ProcessInfo.processInfo.environment[\"MOBENCH_IOS_BENCHMARK_TIMEOUT_SECS\"]"
3104            ),
3105            "refreshed BenchRunnerUITests.swift should honor runtime timeout overrides, got:\n{}",
3106            refreshed
3107        );
3108        assert!(
3109            refreshed.contains(
3110                "private let expectedBenchmarkFunction = \"sample_fns::example_benchmark\""
3111            ),
3112            "generated XCUITest must bind the report to the requested function, got:\n{}",
3113            refreshed
3114        );
3115        for validator in [
3116            "JSONSerialization.jsonObject",
3117            "XCTAssertNil(payload[\"error\"]",
3118            "reportedFunction",
3119            "payload[\"samples_ns\"]",
3120            "payload[\"samples\"]",
3121            "payload[\"schema_version\"]",
3122        ] {
3123            assert!(
3124                refreshed.contains(validator),
3125                "generated XCUITest omitted `{validator}`, got:\n{refreshed}"
3126            );
3127        }
3128
3129        fs::remove_dir_all(&temp_dir).ok();
3130    }
3131
3132    #[test]
3133    fn test_generate_ios_project_uses_configured_benchmark_timeout() {
3134        let temp_dir = env::temp_dir().join("mobench-sdk-ios-timeout-test");
3135        let _ = fs::remove_dir_all(&temp_dir);
3136        fs::create_dir_all(&temp_dir).unwrap();
3137
3138        let result = generate_ios_project_with_timeout(
3139            &temp_dir,
3140            "sample_fns",
3141            "BenchRunner",
3142            "dev.world.samplefns",
3143            "sample_fns::example_benchmark",
3144            1200,
3145            crate::FfiBackend::Uniffi,
3146        );
3147
3148        assert!(result.is_ok(), "generate_ios_project should succeed");
3149
3150        let ui_test_path =
3151            temp_dir.join("ios/BenchRunner/BenchRunnerUITests/BenchRunnerUITests.swift");
3152        let contents = fs::read_to_string(&ui_test_path).unwrap();
3153        assert!(
3154            contents.contains("private let defaultBenchmarkTimeout: TimeInterval = 1200.0"),
3155            "generated BenchRunnerUITests.swift should embed the configured timeout, got:\n{}",
3156            contents
3157        );
3158
3159        fs::remove_dir_all(&temp_dir).ok();
3160    }
3161
3162    #[test]
3163    fn test_resolve_ios_benchmark_timeout_secs_defaults_invalid_values() {
3164        assert_eq!(resolve_ios_benchmark_timeout_secs(None), 300);
3165        assert_eq!(resolve_ios_benchmark_timeout_secs(Some("900")), 900);
3166        assert_eq!(resolve_ios_benchmark_timeout_secs(Some("0")), 300);
3167        assert_eq!(resolve_ios_benchmark_timeout_secs(Some("bogus")), 300);
3168    }
3169
3170    #[test]
3171    fn test_cross_platform_naming_consistency() {
3172        // Test that Android and iOS use the same naming convention for package/bundle IDs
3173        let temp_dir = env::temp_dir().join("mobench-sdk-naming-consistency-test");
3174        let _ = fs::remove_dir_all(&temp_dir);
3175        fs::create_dir_all(&temp_dir).unwrap();
3176
3177        let project_name = "bench-mobile";
3178
3179        // Generate Android project
3180        let result = generate_android_project(&temp_dir, project_name, "bench_mobile::test_func");
3181        assert!(
3182            result.is_ok(),
3183            "generate_android_project failed: {:?}",
3184            result.err()
3185        );
3186
3187        // Generate iOS project (mimicking how ensure_ios_project does it)
3188        let bundle_id_component = sanitize_bundle_id_component(project_name);
3189        let bundle_prefix = format!("dev.world.{}", bundle_id_component);
3190        let result = generate_ios_project(
3191            &temp_dir,
3192            &project_name.replace('-', "_"),
3193            "BenchRunner",
3194            &bundle_prefix,
3195            "bench_mobile::test_func",
3196        );
3197        assert!(
3198            result.is_ok(),
3199            "generate_ios_project failed: {:?}",
3200            result.err()
3201        );
3202
3203        // Read Android build.gradle to extract package name
3204        let android_build_gradle = fs::read_to_string(temp_dir.join("android/app/build.gradle"))
3205            .expect("Failed to read Android build.gradle");
3206
3207        // Read iOS project.yml to extract bundle ID prefix
3208        let ios_project_yml = fs::read_to_string(temp_dir.join("ios/BenchRunner/project.yml"))
3209            .expect("Failed to read iOS project.yml");
3210
3211        // Both should use "benchmobile" (without hyphens or underscores)
3212        // Android: namespace = "dev.world.benchmobile"
3213        // iOS: bundleIdPrefix: dev.world.benchmobile
3214        assert!(
3215            android_build_gradle.contains("dev.world.benchmobile"),
3216            "Android package should be 'dev.world.benchmobile', got:\n{}",
3217            android_build_gradle
3218        );
3219        assert!(
3220            ios_project_yml.contains("dev.world.benchmobile"),
3221            "iOS bundle prefix should contain 'dev.world.benchmobile', got:\n{}",
3222            ios_project_yml
3223        );
3224
3225        // Ensure Android doesn't use hyphens or underscores in the package ID component
3226        assert!(
3227            !android_build_gradle.contains("dev.world.bench-mobile"),
3228            "Android package should NOT contain hyphens"
3229        );
3230        assert!(
3231            !android_build_gradle.contains("dev.world.bench_mobile"),
3232            "Android package should NOT contain underscores"
3233        );
3234
3235        // Cleanup
3236        fs::remove_dir_all(&temp_dir).ok();
3237    }
3238
3239    #[test]
3240    fn test_cross_platform_version_consistency() {
3241        // Test that Android and iOS use the same version strings
3242        let temp_dir = env::temp_dir().join("mobench-sdk-version-consistency-test");
3243        let _ = fs::remove_dir_all(&temp_dir);
3244        fs::create_dir_all(&temp_dir).unwrap();
3245
3246        let project_name = "test-project";
3247
3248        // Generate Android project
3249        let result = generate_android_project(&temp_dir, project_name, "test_project::test_func");
3250        assert!(
3251            result.is_ok(),
3252            "generate_android_project failed: {:?}",
3253            result.err()
3254        );
3255
3256        // Generate iOS project
3257        let bundle_id_component = sanitize_bundle_id_component(project_name);
3258        let bundle_prefix = format!("dev.world.{}", bundle_id_component);
3259        let result = generate_ios_project(
3260            &temp_dir,
3261            &project_name.replace('-', "_"),
3262            "BenchRunner",
3263            &bundle_prefix,
3264            "test_project::test_func",
3265        );
3266        assert!(
3267            result.is_ok(),
3268            "generate_ios_project failed: {:?}",
3269            result.err()
3270        );
3271
3272        // Read Android build.gradle
3273        let android_build_gradle = fs::read_to_string(temp_dir.join("android/app/build.gradle"))
3274            .expect("Failed to read Android build.gradle");
3275
3276        // Read iOS project.yml
3277        let ios_project_yml = fs::read_to_string(temp_dir.join("ios/BenchRunner/project.yml"))
3278            .expect("Failed to read iOS project.yml");
3279
3280        // Both should use version "1.0.0"
3281        assert!(
3282            android_build_gradle.contains("versionName \"1.0.0\""),
3283            "Android versionName should be '1.0.0', got:\n{}",
3284            android_build_gradle
3285        );
3286        assert!(
3287            ios_project_yml.contains("CFBundleShortVersionString: \"1.0.0\""),
3288            "iOS CFBundleShortVersionString should be '1.0.0', got:\n{}",
3289            ios_project_yml
3290        );
3291
3292        // Cleanup
3293        fs::remove_dir_all(&temp_dir).ok();
3294    }
3295
3296    #[test]
3297    fn test_bundle_id_prefix_consistency() {
3298        // Test that the bundle ID prefix format is consistent across platforms
3299        let test_cases = vec![
3300            ("my-project", "dev.world.myproject"),
3301            ("bench_mobile", "dev.world.benchmobile"),
3302            ("TestApp", "dev.world.testapp"),
3303            ("app-with-many-dashes", "dev.world.appwithmanydashes"),
3304            (
3305                "app_with_many_underscores",
3306                "dev.world.appwithmanyunderscores",
3307            ),
3308        ];
3309
3310        for (input, expected_prefix) in test_cases {
3311            let sanitized = sanitize_bundle_id_component(input);
3312            let full_prefix = format!("dev.world.{}", sanitized);
3313            assert_eq!(
3314                full_prefix, expected_prefix,
3315                "For input '{}', expected '{}' but got '{}'",
3316                input, expected_prefix, full_prefix
3317            );
3318        }
3319    }
3320}