Skip to main content

mobench_sdk/
codegen.rs

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