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");
15
16/// Template variable that can be replaced in template files
17#[derive(Debug, Clone)]
18pub struct TemplateVar {
19    pub name: &'static str,
20    pub value: String,
21}
22
23/// Generates a new mobile benchmark project from templates
24///
25/// Creates the necessary directory structure and files for benchmarking on
26/// mobile platforms. This includes:
27/// - A `bench-mobile/` crate for FFI bindings
28/// - Platform-specific app projects (Android and/or iOS)
29/// - Configuration files
30///
31/// # Arguments
32///
33/// * `config` - Configuration for project initialization
34///
35/// # Returns
36///
37/// * `Ok(PathBuf)` - Path to the generated project root
38/// * `Err(BenchError)` - If generation fails
39pub fn generate_project(config: &InitConfig) -> Result<PathBuf, BenchError> {
40    let output_dir = &config.output_dir;
41    let project_slug = sanitize_package_name(&config.project_name);
42    let project_pascal = to_pascal_case(&project_slug);
43    // Use sanitized bundle ID component (alphanumeric only) to avoid iOS validation issues
44    let bundle_id_component = sanitize_bundle_id_component(&project_slug);
45    let bundle_prefix = format!("dev.world.{}", bundle_id_component);
46
47    // Create base directories
48    fs::create_dir_all(output_dir)?;
49
50    // Generate bench-mobile FFI wrapper crate
51    generate_bench_mobile_crate(output_dir, &project_slug)?;
52
53    // For full project generation (init), use "example_fibonacci" as the default
54    // since the generated example benchmarks include this function
55    let default_function = "example_fibonacci";
56
57    // Generate platform-specific projects
58    match config.target {
59        Target::Android => {
60            generate_android_project(output_dir, &project_slug, default_function)?;
61        }
62        Target::Ios => {
63            generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix, default_function)?;
64        }
65        Target::Both => {
66            generate_android_project(output_dir, &project_slug, default_function)?;
67            generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix, default_function)?;
68        }
69    }
70
71    // Generate config file
72    generate_config_file(output_dir, config)?;
73
74    // Generate examples if requested
75    if config.generate_examples {
76        generate_example_benchmarks(output_dir)?;
77    }
78
79    Ok(output_dir.clone())
80}
81
82/// Generates the bench-mobile FFI wrapper crate
83fn generate_bench_mobile_crate(output_dir: &Path, project_name: &str) -> Result<(), BenchError> {
84    let crate_dir = output_dir.join("bench-mobile");
85    fs::create_dir_all(crate_dir.join("src"))?;
86
87    let crate_name = format!("{}-bench-mobile", project_name);
88
89    // Generate Cargo.toml
90    // Note: We configure rustls to use 'ring' instead of 'aws-lc-rs' (default in rustls 0.23+)
91    // because aws-lc-rs doesn't compile for Android NDK targets.
92    let cargo_toml = format!(
93        r#"[package]
94name = "{}"
95version = "0.1.0"
96edition = "2021"
97
98[lib]
99crate-type = ["cdylib", "staticlib", "rlib"]
100
101[dependencies]
102mobench-sdk = {{ path = ".." }}
103uniffi = "0.28"
104{} = {{ path = ".." }}
105
106[features]
107default = []
108
109[build-dependencies]
110uniffi = {{ version = "0.28", features = ["build"] }}
111
112# Binary for generating UniFFI bindings (used by mobench build)
113[[bin]]
114name = "uniffi-bindgen"
115path = "src/bin/uniffi-bindgen.rs"
116
117# IMPORTANT: If your project uses rustls (directly or transitively), you must configure
118# it to use the 'ring' crypto backend instead of 'aws-lc-rs' (the default in rustls 0.23+).
119# aws-lc-rs doesn't compile for Android NDK targets due to C compilation issues.
120#
121# Add this to your root Cargo.toml:
122# [workspace.dependencies]
123# rustls = {{ version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }}
124#
125# Then in each crate that uses rustls:
126# [dependencies]
127# rustls = {{ workspace = true }}
128"#,
129        crate_name, project_name
130    );
131
132    fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
133
134    // Generate src/lib.rs
135    let lib_rs_template = r#"//! Mobile FFI bindings for benchmarks
136//!
137//! This crate provides the FFI boundary between Rust benchmarks and mobile
138//! platforms (Android/iOS). It uses UniFFI to generate type-safe bindings.
139
140use uniffi;
141
142// Ensure the user crate is linked so benchmark registrations are pulled in.
143extern crate {{USER_CRATE}} as _bench_user_crate;
144
145// Re-export mobench-sdk types with UniFFI annotations
146#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
147pub struct BenchSpec {
148    pub name: String,
149    pub iterations: u32,
150    pub warmup: u32,
151}
152
153#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
154pub struct BenchSample {
155    pub duration_ns: u64,
156}
157
158#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
159pub struct BenchReport {
160    pub spec: BenchSpec,
161    pub samples: Vec<BenchSample>,
162}
163
164#[derive(Debug, thiserror::Error, uniffi::Error)]
165#[uniffi(flat_error)]
166pub enum BenchError {
167    #[error("iterations must be greater than zero")]
168    InvalidIterations,
169
170    #[error("unknown benchmark function: {name}")]
171    UnknownFunction { name: String },
172
173    #[error("benchmark execution failed: {reason}")]
174    ExecutionFailed { reason: String },
175}
176
177// Convert from mobench-sdk types
178impl From<mobench_sdk::BenchSpec> for BenchSpec {
179    fn from(spec: mobench_sdk::BenchSpec) -> Self {
180        Self {
181            name: spec.name,
182            iterations: spec.iterations,
183            warmup: spec.warmup,
184        }
185    }
186}
187
188impl From<BenchSpec> for mobench_sdk::BenchSpec {
189    fn from(spec: BenchSpec) -> Self {
190        Self {
191            name: spec.name,
192            iterations: spec.iterations,
193            warmup: spec.warmup,
194        }
195    }
196}
197
198impl From<mobench_sdk::BenchSample> for BenchSample {
199    fn from(sample: mobench_sdk::BenchSample) -> Self {
200        Self {
201            duration_ns: sample.duration_ns,
202        }
203    }
204}
205
206impl From<mobench_sdk::RunnerReport> for BenchReport {
207    fn from(report: mobench_sdk::RunnerReport) -> Self {
208        Self {
209            spec: report.spec.into(),
210            samples: report.samples.into_iter().map(Into::into).collect(),
211        }
212    }
213}
214
215impl From<mobench_sdk::BenchError> for BenchError {
216    fn from(err: mobench_sdk::BenchError) -> Self {
217        match err {
218            mobench_sdk::BenchError::Runner(runner_err) => {
219                BenchError::ExecutionFailed {
220                    reason: runner_err.to_string(),
221                }
222            }
223            mobench_sdk::BenchError::UnknownFunction(name, _available) => {
224                BenchError::UnknownFunction { name }
225            }
226            _ => BenchError::ExecutionFailed {
227                reason: err.to_string(),
228            },
229        }
230    }
231}
232
233/// Runs a benchmark by name with the given specification
234///
235/// This is the main FFI entry point called from mobile platforms.
236#[uniffi::export]
237pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
238    let sdk_spec: mobench_sdk::BenchSpec = spec.into();
239    let report = mobench_sdk::run_benchmark(sdk_spec)?;
240    Ok(report.into())
241}
242
243// Generate UniFFI scaffolding
244uniffi::setup_scaffolding!();
245"#;
246
247    let lib_rs = render_template(
248        lib_rs_template,
249        &[TemplateVar {
250            name: "USER_CRATE",
251            value: project_name.replace('-', "_"),
252        }],
253    );
254    fs::write(crate_dir.join("src/lib.rs"), lib_rs)?;
255
256    // Generate build.rs
257    let build_rs = r#"fn main() {
258    uniffi::generate_scaffolding("src/lib.rs").unwrap();
259}
260"#;
261
262    fs::write(crate_dir.join("build.rs"), build_rs)?;
263
264    // Generate uniffi-bindgen binary (used by mobench build)
265    let bin_dir = crate_dir.join("src/bin");
266    fs::create_dir_all(&bin_dir)?;
267    let uniffi_bindgen_rs = r#"fn main() {
268    uniffi::uniffi_bindgen_main()
269}
270"#;
271    fs::write(bin_dir.join("uniffi-bindgen.rs"), uniffi_bindgen_rs)?;
272
273    Ok(())
274}
275
276/// Generates Android project structure from templates
277///
278/// This function can be called standalone to generate just the Android
279/// project scaffolding, useful for auto-generation during build.
280///
281/// # Arguments
282///
283/// * `output_dir` - Directory to write the `android/` project into
284/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
285/// * `default_function` - Default benchmark function to use (e.g., "bench_mobile::my_benchmark")
286pub fn generate_android_project(
287    output_dir: &Path,
288    project_slug: &str,
289    default_function: &str,
290) -> Result<(), BenchError> {
291    let target_dir = output_dir.join("android");
292    let library_name = project_slug.replace('-', "_");
293    let project_pascal = to_pascal_case(project_slug);
294    // Use sanitized bundle ID component (alphanumeric only) for consistency with iOS
295    // This ensures both platforms use the same naming convention: "benchmobile" not "bench-mobile"
296    let package_id_component = sanitize_bundle_id_component(project_slug);
297    let package_name = format!("dev.world.{}", package_id_component);
298    let vars = vec![
299        TemplateVar {
300            name: "PROJECT_NAME",
301            value: project_slug.to_string(),
302        },
303        TemplateVar {
304            name: "PROJECT_NAME_PASCAL",
305            value: project_pascal.clone(),
306        },
307        TemplateVar {
308            name: "APP_NAME",
309            value: format!("{} Benchmark", project_pascal),
310        },
311        TemplateVar {
312            name: "PACKAGE_NAME",
313            value: package_name.clone(),
314        },
315        TemplateVar {
316            name: "UNIFFI_NAMESPACE",
317            value: library_name.clone(),
318        },
319        TemplateVar {
320            name: "LIBRARY_NAME",
321            value: library_name,
322        },
323        TemplateVar {
324            name: "DEFAULT_FUNCTION",
325            value: default_function.to_string(),
326        },
327    ];
328    render_dir(&ANDROID_TEMPLATES, &target_dir, &vars)?;
329
330    // Move Kotlin files to the correct package directory structure
331    // The package "dev.world.{project_slug}" maps to directory "dev/world/{project_slug}/"
332    move_kotlin_files_to_package_dir(&target_dir, &package_name)?;
333
334    Ok(())
335}
336
337/// Moves Kotlin source files to the correct package directory structure
338///
339/// Android requires source files to be in directories matching their package declaration.
340/// For example, a file with `package dev.world.my_project` must be in
341/// `app/src/main/java/dev/world/my_project/`.
342///
343/// This function moves:
344/// - MainActivity.kt from `app/src/main/java/` to `app/src/main/java/{package_path}/`
345/// - MainActivityTest.kt from `app/src/androidTest/java/` to `app/src/androidTest/java/{package_path}/`
346fn move_kotlin_files_to_package_dir(android_dir: &Path, package_name: &str) -> Result<(), BenchError> {
347    // Convert package name to directory path (e.g., "dev.world.my_project" -> "dev/world/my_project")
348    let package_path = package_name.replace('.', "/");
349
350    // Move main source files
351    let main_java_dir = android_dir.join("app/src/main/java");
352    let main_package_dir = main_java_dir.join(&package_path);
353    move_kotlin_file(&main_java_dir, &main_package_dir, "MainActivity.kt")?;
354
355    // Move test source files
356    let test_java_dir = android_dir.join("app/src/androidTest/java");
357    let test_package_dir = test_java_dir.join(&package_path);
358    move_kotlin_file(&test_java_dir, &test_package_dir, "MainActivityTest.kt")?;
359
360    Ok(())
361}
362
363/// Moves a single Kotlin file from source directory to package directory
364fn move_kotlin_file(src_dir: &Path, dest_dir: &Path, filename: &str) -> Result<(), BenchError> {
365    let src_file = src_dir.join(filename);
366    if !src_file.exists() {
367        // File doesn't exist in source, nothing to move
368        return Ok(());
369    }
370
371    // Create the package directory if it doesn't exist
372    fs::create_dir_all(dest_dir).map_err(|e| {
373        BenchError::Build(format!(
374            "Failed to create package directory {:?}: {}",
375            dest_dir, e
376        ))
377    })?;
378
379    let dest_file = dest_dir.join(filename);
380
381    // Move the file (copy + delete for cross-filesystem compatibility)
382    fs::copy(&src_file, &dest_file).map_err(|e| {
383        BenchError::Build(format!(
384            "Failed to copy {} to {:?}: {}",
385            filename, dest_file, e
386        ))
387    })?;
388
389    fs::remove_file(&src_file).map_err(|e| {
390        BenchError::Build(format!(
391            "Failed to remove original file {:?}: {}",
392            src_file, e
393        ))
394    })?;
395
396    Ok(())
397}
398
399/// Generates iOS project structure from templates
400///
401/// This function can be called standalone to generate just the iOS
402/// project scaffolding, useful for auto-generation during build.
403///
404/// # Arguments
405///
406/// * `output_dir` - Directory to write the `ios/` project into
407/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
408/// * `project_pascal` - PascalCase version of project name (e.g., "BenchMobile")
409/// * `bundle_prefix` - iOS bundle ID prefix (e.g., "dev.world.bench")
410/// * `default_function` - Default benchmark function to use (e.g., "bench_mobile::my_benchmark")
411pub fn generate_ios_project(
412    output_dir: &Path,
413    project_slug: &str,
414    project_pascal: &str,
415    bundle_prefix: &str,
416    default_function: &str,
417) -> Result<(), BenchError> {
418    let target_dir = output_dir.join("ios");
419    // Sanitize bundle ID components to ensure they only contain alphanumeric characters
420    // iOS bundle identifiers should not contain hyphens or underscores
421    let sanitized_bundle_prefix = {
422        let parts: Vec<&str> = bundle_prefix.split('.').collect();
423        parts.iter()
424            .map(|part| sanitize_bundle_id_component(part))
425            .collect::<Vec<_>>()
426            .join(".")
427    };
428    // Use the actual app name (project_pascal, e.g., "BenchRunner") for the bundle ID suffix,
429    // not the crate name again. This prevents duplication like "dev.world.benchmobile.benchmobile"
430    // and produces the correct "dev.world.benchmobile.BenchRunner"
431    let vars = vec![
432        TemplateVar {
433            name: "DEFAULT_FUNCTION",
434            value: default_function.to_string(),
435        },
436        TemplateVar {
437            name: "PROJECT_NAME_PASCAL",
438            value: project_pascal.to_string(),
439        },
440        TemplateVar {
441            name: "BUNDLE_ID_PREFIX",
442            value: sanitized_bundle_prefix.clone(),
443        },
444        TemplateVar {
445            name: "BUNDLE_ID",
446            value: format!("{}.{}", sanitized_bundle_prefix, project_pascal),
447        },
448        TemplateVar {
449            name: "LIBRARY_NAME",
450            value: project_slug.replace('-', "_"),
451        },
452    ];
453    render_dir(&IOS_TEMPLATES, &target_dir, &vars)?;
454    Ok(())
455}
456
457/// Generates bench-config.toml configuration file
458fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
459    let config_target = match config.target {
460        Target::Ios => "ios",
461        Target::Android | Target::Both => "android",
462    };
463    let config_content = format!(
464        r#"# mobench configuration
465# This file controls how benchmarks are executed on devices.
466
467target = "{}"
468function = "example_fibonacci"
469iterations = 100
470warmup = 10
471device_matrix = "device-matrix.yaml"
472device_tags = ["default"]
473
474[browserstack]
475app_automate_username = "${{BROWSERSTACK_USERNAME}}"
476app_automate_access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
477project = "{}-benchmarks"
478
479[ios_xcuitest]
480app = "target/ios/BenchRunner.ipa"
481test_suite = "target/ios/BenchRunnerUITests.zip"
482"#,
483        config_target, config.project_name
484    );
485
486    fs::write(output_dir.join("bench-config.toml"), config_content)?;
487
488    Ok(())
489}
490
491/// Generates example benchmark functions
492fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
493    let examples_dir = output_dir.join("benches");
494    fs::create_dir_all(&examples_dir)?;
495
496    let example_content = r#"//! Example benchmarks
497//!
498//! This file demonstrates how to write benchmarks with mobench-sdk.
499
500use mobench_sdk::benchmark;
501
502/// Simple benchmark example
503#[benchmark]
504fn example_fibonacci() {
505    let result = fibonacci(30);
506    std::hint::black_box(result);
507}
508
509/// Another example with a loop
510#[benchmark]
511fn example_sum() {
512    let mut sum = 0u64;
513    for i in 0..10000 {
514        sum = sum.wrapping_add(i);
515    }
516    std::hint::black_box(sum);
517}
518
519// Helper function (not benchmarked)
520fn fibonacci(n: u32) -> u64 {
521    match n {
522        0 => 0,
523        1 => 1,
524        _ => {
525            let mut a = 0u64;
526            let mut b = 1u64;
527            for _ in 2..=n {
528                let next = a.wrapping_add(b);
529                a = b;
530                b = next;
531            }
532            b
533        }
534    }
535}
536"#;
537
538    fs::write(examples_dir.join("example.rs"), example_content)?;
539
540    Ok(())
541}
542
543/// File extensions that should be processed for template variable substitution
544const TEMPLATE_EXTENSIONS: &[&str] = &[
545    "gradle", "xml", "kt", "java", "swift", "yml", "yaml", "json", "toml", "md", "txt", "h", "m",
546    "plist", "pbxproj", "xcscheme", "xcworkspacedata", "entitlements", "modulemap",
547];
548
549fn render_dir(
550    dir: &Dir,
551    out_root: &Path,
552    vars: &[TemplateVar],
553) -> Result<(), BenchError> {
554    for entry in dir.entries() {
555        match entry {
556            DirEntry::Dir(sub) => {
557                // Skip cache directories
558                if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
559                    continue;
560                }
561                render_dir(sub, out_root, vars)?;
562            }
563            DirEntry::File(file) => {
564                if file.path().components().any(|c| c.as_os_str() == ".gradle") {
565                    continue;
566                }
567                // file.path() returns the full relative path from the embedded dir root
568                let mut relative = file.path().to_path_buf();
569                let mut contents = file.contents().to_vec();
570
571                // Check if file has .template extension (explicit template)
572                let is_explicit_template = relative
573                    .extension()
574                    .map(|ext| ext == "template")
575                    .unwrap_or(false);
576
577                // Check if file is a text file that should be processed for templates
578                let should_render = is_explicit_template || is_template_file(&relative);
579
580                if is_explicit_template {
581                    // Remove .template extension from output filename
582                    relative.set_extension("");
583                }
584
585                if should_render {
586                    if let Ok(text) = std::str::from_utf8(&contents) {
587                        let rendered = render_template(text, vars);
588                        // Validate that all template variables were replaced
589                        validate_no_unreplaced_placeholders(&rendered, &relative)?;
590                        contents = rendered.into_bytes();
591                    }
592                }
593
594                let out_path = out_root.join(relative);
595                if let Some(parent) = out_path.parent() {
596                    fs::create_dir_all(parent)?;
597                }
598                fs::write(&out_path, contents)?;
599            }
600        }
601    }
602    Ok(())
603}
604
605/// Checks if a file should be processed for template variable substitution
606/// based on its extension
607fn is_template_file(path: &Path) -> bool {
608    // Check for .template extension on any file
609    if let Some(ext) = path.extension() {
610        if ext == "template" {
611            return true;
612        }
613        // Check if the base extension is in our list
614        if let Some(ext_str) = ext.to_str() {
615            return TEMPLATE_EXTENSIONS.contains(&ext_str);
616        }
617    }
618    // Also check the filename without the .template extension
619    if let Some(stem) = path.file_stem() {
620        let stem_path = Path::new(stem);
621        if let Some(ext) = stem_path.extension() {
622            if let Some(ext_str) = ext.to_str() {
623                return TEMPLATE_EXTENSIONS.contains(&ext_str);
624            }
625        }
626    }
627    false
628}
629
630/// Validates that no unreplaced template placeholders remain in the rendered content
631fn validate_no_unreplaced_placeholders(content: &str, file_path: &Path) -> Result<(), BenchError> {
632    // Find all {{...}} patterns
633    let mut pos = 0;
634    let mut unreplaced = Vec::new();
635
636    while let Some(start) = content[pos..].find("{{") {
637        let abs_start = pos + start;
638        if let Some(end) = content[abs_start..].find("}}") {
639            let placeholder = &content[abs_start..abs_start + end + 2];
640            // Extract just the variable name
641            let var_name = &content[abs_start + 2..abs_start + end];
642            // Skip placeholders that look like Gradle variable syntax (e.g., ${...})
643            // or other non-template patterns
644            if !var_name.contains('$') && !var_name.contains(' ') && !var_name.is_empty() {
645                unreplaced.push(placeholder.to_string());
646            }
647            pos = abs_start + end + 2;
648        } else {
649            break;
650        }
651    }
652
653    if !unreplaced.is_empty() {
654        return Err(BenchError::Build(format!(
655            "Template validation failed for {:?}: unreplaced placeholders found: {:?}\n\n\
656             This is a bug in mobench-sdk. Please report it at:\n\
657             https://github.com/worldcoin/mobile-bench-rs/issues",
658            file_path, unreplaced
659        )));
660    }
661
662    Ok(())
663}
664
665fn render_template(input: &str, vars: &[TemplateVar]) -> String {
666    let mut output = input.to_string();
667    for var in vars {
668        output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
669    }
670    output
671}
672
673/// Sanitizes a string to be a valid iOS bundle identifier component
674///
675/// Bundle identifiers can only contain alphanumeric characters (A-Z, a-z, 0-9),
676/// hyphens (-), and dots (.). However, to avoid issues and maintain consistency,
677/// this function converts all non-alphanumeric characters to lowercase letters only.
678///
679/// Examples:
680/// - "bench-mobile" -> "benchmobile"
681/// - "bench_mobile" -> "benchmobile"
682/// - "my-project_name" -> "myprojectname"
683pub fn sanitize_bundle_id_component(name: &str) -> String {
684    name.chars()
685        .filter(|c| c.is_ascii_alphanumeric())
686        .collect::<String>()
687        .to_lowercase()
688}
689
690fn sanitize_package_name(name: &str) -> String {
691    name.chars()
692        .map(|c| {
693            if c.is_ascii_alphanumeric() {
694                c.to_ascii_lowercase()
695            } else {
696                '-'
697            }
698        })
699        .collect::<String>()
700        .trim_matches('-')
701        .replace("--", "-")
702}
703
704/// Converts a string to PascalCase
705pub fn to_pascal_case(input: &str) -> String {
706    input
707        .split(|c: char| !c.is_ascii_alphanumeric())
708        .filter(|s| !s.is_empty())
709        .map(|s| {
710            let mut chars = s.chars();
711            let first = chars.next().unwrap().to_ascii_uppercase();
712            let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
713            format!("{}{}", first, rest)
714        })
715        .collect::<String>()
716}
717
718/// Checks if the Android project scaffolding exists at the given output directory
719///
720/// Returns true if the `android/build.gradle` or `android/build.gradle.kts` file exists.
721pub fn android_project_exists(output_dir: &Path) -> bool {
722    let android_dir = output_dir.join("android");
723    android_dir.join("build.gradle").exists() || android_dir.join("build.gradle.kts").exists()
724}
725
726/// Checks if the iOS project scaffolding exists at the given output directory
727///
728/// Returns true if the `ios/BenchRunner/project.yml` file exists.
729pub fn ios_project_exists(output_dir: &Path) -> bool {
730    output_dir.join("ios/BenchRunner/project.yml").exists()
731}
732
733/// Detects the first benchmark function in a crate by scanning src/lib.rs for `#[benchmark]`
734///
735/// This function looks for functions marked with the `#[benchmark]` attribute and returns
736/// the first one found in the format `{crate_name}::{function_name}`.
737///
738/// # Arguments
739///
740/// * `crate_dir` - Path to the crate directory containing Cargo.toml
741/// * `crate_name` - Name of the crate (used as prefix for the function name)
742///
743/// # Returns
744///
745/// * `Some(String)` - The detected function name in format `crate_name::function_name`
746/// * `None` - If no benchmark functions are found or if the file cannot be read
747pub fn detect_default_function(crate_dir: &Path, crate_name: &str) -> Option<String> {
748    let lib_rs = crate_dir.join("src/lib.rs");
749    if !lib_rs.exists() {
750        return None;
751    }
752
753    let file = fs::File::open(&lib_rs).ok()?;
754    let reader = BufReader::new(file);
755
756    let mut found_benchmark_attr = false;
757    let crate_name_normalized = crate_name.replace('-', "_");
758
759    for line in reader.lines().map_while(Result::ok) {
760        let trimmed = line.trim();
761
762        // Check for #[benchmark] attribute
763        if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
764            found_benchmark_attr = true;
765            continue;
766        }
767
768        // If we found a benchmark attribute, look for the function definition
769        if found_benchmark_attr {
770            // Look for "fn function_name" or "pub fn function_name"
771            if let Some(fn_pos) = trimmed.find("fn ") {
772                let after_fn = &trimmed[fn_pos + 3..];
773                // Extract function name (until '(' or whitespace)
774                let fn_name: String = after_fn
775                    .chars()
776                    .take_while(|c| c.is_alphanumeric() || *c == '_')
777                    .collect();
778
779                if !fn_name.is_empty() {
780                    return Some(format!("{}::{}", crate_name_normalized, fn_name));
781                }
782            }
783            // Reset if we hit a line that's not a function definition
784            // (could be another attribute or comment)
785            if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
786                found_benchmark_attr = false;
787            }
788        }
789    }
790
791    None
792}
793
794/// Detects all benchmark functions in a crate by scanning src/lib.rs for `#[benchmark]`
795///
796/// This function looks for functions marked with the `#[benchmark]` attribute and returns
797/// all found in the format `{crate_name}::{function_name}`.
798///
799/// # Arguments
800///
801/// * `crate_dir` - Path to the crate directory containing Cargo.toml
802/// * `crate_name` - Name of the crate (used as prefix for the function names)
803///
804/// # Returns
805///
806/// A vector of benchmark function names in format `crate_name::function_name`
807pub fn detect_all_benchmarks(crate_dir: &Path, crate_name: &str) -> Vec<String> {
808    let lib_rs = crate_dir.join("src/lib.rs");
809    if !lib_rs.exists() {
810        return Vec::new();
811    }
812
813    let Ok(file) = fs::File::open(&lib_rs) else {
814        return Vec::new();
815    };
816    let reader = BufReader::new(file);
817
818    let mut benchmarks = Vec::new();
819    let mut found_benchmark_attr = false;
820    let crate_name_normalized = crate_name.replace('-', "_");
821
822    for line in reader.lines().map_while(Result::ok) {
823        let trimmed = line.trim();
824
825        // Check for #[benchmark] attribute
826        if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
827            found_benchmark_attr = true;
828            continue;
829        }
830
831        // If we found a benchmark attribute, look for the function definition
832        if found_benchmark_attr {
833            // Look for "fn function_name" or "pub fn function_name"
834            if let Some(fn_pos) = trimmed.find("fn ") {
835                let after_fn = &trimmed[fn_pos + 3..];
836                // Extract function name (until '(' or whitespace)
837                let fn_name: String = after_fn
838                    .chars()
839                    .take_while(|c| c.is_alphanumeric() || *c == '_')
840                    .collect();
841
842                if !fn_name.is_empty() {
843                    benchmarks.push(format!("{}::{}", crate_name_normalized, fn_name));
844                }
845                found_benchmark_attr = false;
846            }
847            // Reset if we hit a line that's not a function definition
848            // (could be another attribute or comment)
849            if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
850                found_benchmark_attr = false;
851            }
852        }
853    }
854
855    benchmarks
856}
857
858/// Validates that a benchmark function exists in the crate source
859///
860/// # Arguments
861///
862/// * `crate_dir` - Path to the crate directory containing Cargo.toml
863/// * `crate_name` - Name of the crate (used as prefix for the function names)
864/// * `function_name` - The function name to validate (with or without crate prefix)
865///
866/// # Returns
867///
868/// `true` if the function is found, `false` otherwise
869pub fn validate_benchmark_exists(crate_dir: &Path, crate_name: &str, function_name: &str) -> bool {
870    let benchmarks = detect_all_benchmarks(crate_dir, crate_name);
871    let crate_name_normalized = crate_name.replace('-', "_");
872
873    // Normalize the function name - add crate prefix if missing
874    let normalized_name = if function_name.contains("::") {
875        function_name.to_string()
876    } else {
877        format!("{}::{}", crate_name_normalized, function_name)
878    };
879
880    benchmarks.iter().any(|b| b == &normalized_name)
881}
882
883/// Resolves the default benchmark function for a project
884///
885/// This function attempts to auto-detect benchmark functions from the crate's source.
886/// If no benchmarks are found, it falls back to a sensible default based on the crate name.
887///
888/// # Arguments
889///
890/// * `project_root` - Root directory of the project
891/// * `crate_name` - Name of the benchmark crate
892/// * `crate_dir` - Optional explicit crate directory (if None, will search standard locations)
893///
894/// # Returns
895///
896/// The default function name in format `crate_name::function_name`
897pub fn resolve_default_function(
898    project_root: &Path,
899    crate_name: &str,
900    crate_dir: Option<&Path>,
901) -> String {
902    let crate_name_normalized = crate_name.replace('-', "_");
903
904    // Try to find the crate directory
905    let search_dirs: Vec<PathBuf> = if let Some(dir) = crate_dir {
906        vec![dir.to_path_buf()]
907    } else {
908        vec![
909            project_root.join("bench-mobile"),
910            project_root.join("crates").join(crate_name),
911            project_root.to_path_buf(),
912        ]
913    };
914
915    // Try to detect benchmarks from each potential location
916    for dir in &search_dirs {
917        if dir.join("Cargo.toml").exists() {
918            if let Some(detected) = detect_default_function(dir, &crate_name_normalized) {
919                return detected;
920            }
921        }
922    }
923
924    // Fallback: use a sensible default based on crate name
925    format!("{}::example_benchmark", crate_name_normalized)
926}
927
928/// Auto-generates Android project scaffolding from a crate name
929///
930/// This is a convenience function that derives template variables from the
931/// crate name and generates the Android project structure. It auto-detects
932/// the default benchmark function from the crate's source code.
933///
934/// # Arguments
935///
936/// * `output_dir` - Directory to write the `android/` project into
937/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
938pub fn ensure_android_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
939    ensure_android_project_with_options(output_dir, crate_name, None, None)
940}
941
942/// Auto-generates Android project scaffolding with additional options
943///
944/// This is a more flexible version of `ensure_android_project` that allows
945/// specifying a custom default function and/or crate directory.
946///
947/// # Arguments
948///
949/// * `output_dir` - Directory to write the `android/` project into
950/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
951/// * `project_root` - Optional project root for auto-detecting benchmarks (defaults to output_dir parent)
952/// * `crate_dir` - Optional explicit crate directory for benchmark detection
953pub fn ensure_android_project_with_options(
954    output_dir: &Path,
955    crate_name: &str,
956    project_root: Option<&Path>,
957    crate_dir: Option<&Path>,
958) -> Result<(), BenchError> {
959    if android_project_exists(output_dir) {
960        return Ok(());
961    }
962
963    println!("Android project not found, generating scaffolding...");
964    let project_slug = crate_name.replace('-', "_");
965
966    // Resolve the default function by auto-detecting from source
967    let effective_root = project_root.unwrap_or_else(|| {
968        output_dir.parent().unwrap_or(output_dir)
969    });
970    let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
971
972    generate_android_project(output_dir, &project_slug, &default_function)?;
973    println!("  Generated Android project at {:?}", output_dir.join("android"));
974    println!("  Default benchmark function: {}", default_function);
975    Ok(())
976}
977
978/// Auto-generates iOS project scaffolding from a crate name
979///
980/// This is a convenience function that derives template variables from the
981/// crate name and generates the iOS project structure. It auto-detects
982/// the default benchmark function from the crate's source code.
983///
984/// # Arguments
985///
986/// * `output_dir` - Directory to write the `ios/` project into
987/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
988pub fn ensure_ios_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
989    ensure_ios_project_with_options(output_dir, crate_name, None, None)
990}
991
992/// Auto-generates iOS project scaffolding with additional options
993///
994/// This is a more flexible version of `ensure_ios_project` that allows
995/// specifying a custom default function and/or crate directory.
996///
997/// # Arguments
998///
999/// * `output_dir` - Directory to write the `ios/` project into
1000/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
1001/// * `project_root` - Optional project root for auto-detecting benchmarks (defaults to output_dir parent)
1002/// * `crate_dir` - Optional explicit crate directory for benchmark detection
1003pub fn ensure_ios_project_with_options(
1004    output_dir: &Path,
1005    crate_name: &str,
1006    project_root: Option<&Path>,
1007    crate_dir: Option<&Path>,
1008) -> Result<(), BenchError> {
1009    if ios_project_exists(output_dir) {
1010        return Ok(());
1011    }
1012
1013    println!("iOS project not found, generating scaffolding...");
1014    // Use fixed "BenchRunner" for project/scheme name to match template directory structure
1015    let project_pascal = "BenchRunner";
1016    // Derive library name and bundle prefix from crate name
1017    let library_name = crate_name.replace('-', "_");
1018    // Use sanitized bundle ID component (alphanumeric only) to avoid iOS validation issues
1019    // e.g., "bench-mobile" or "bench_mobile" -> "benchmobile"
1020    let bundle_id_component = sanitize_bundle_id_component(crate_name);
1021    let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1022
1023    // Resolve the default function by auto-detecting from source
1024    let effective_root = project_root.unwrap_or_else(|| {
1025        output_dir.parent().unwrap_or(output_dir)
1026    });
1027    let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
1028
1029    generate_ios_project(output_dir, &library_name, project_pascal, &bundle_prefix, &default_function)?;
1030    println!("  Generated iOS project at {:?}", output_dir.join("ios"));
1031    println!("  Default benchmark function: {}", default_function);
1032    Ok(())
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    use super::*;
1038    use std::env;
1039
1040    #[test]
1041    fn test_generate_bench_mobile_crate() {
1042        let temp_dir = env::temp_dir().join("mobench-sdk-test");
1043        fs::create_dir_all(&temp_dir).unwrap();
1044
1045        let result = generate_bench_mobile_crate(&temp_dir, "test_project");
1046        assert!(result.is_ok());
1047
1048        // Verify files were created
1049        assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
1050        assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
1051        assert!(temp_dir.join("bench-mobile/build.rs").exists());
1052
1053        // Cleanup
1054        fs::remove_dir_all(&temp_dir).ok();
1055    }
1056
1057    #[test]
1058    fn test_generate_android_project_no_unreplaced_placeholders() {
1059        let temp_dir = env::temp_dir().join("mobench-sdk-android-test");
1060        // Clean up any previous test run
1061        let _ = fs::remove_dir_all(&temp_dir);
1062        fs::create_dir_all(&temp_dir).unwrap();
1063
1064        let result = generate_android_project(&temp_dir, "my-bench-project", "my_bench_project::test_func");
1065        assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
1066
1067        // Verify key files exist
1068        let android_dir = temp_dir.join("android");
1069        assert!(android_dir.join("settings.gradle").exists());
1070        assert!(android_dir.join("app/build.gradle").exists());
1071        assert!(android_dir.join("app/src/main/AndroidManifest.xml").exists());
1072        assert!(android_dir.join("app/src/main/res/values/strings.xml").exists());
1073        assert!(android_dir.join("app/src/main/res/values/themes.xml").exists());
1074
1075        // Verify no unreplaced placeholders remain in generated files
1076        let files_to_check = [
1077            "settings.gradle",
1078            "app/build.gradle",
1079            "app/src/main/AndroidManifest.xml",
1080            "app/src/main/res/values/strings.xml",
1081            "app/src/main/res/values/themes.xml",
1082        ];
1083
1084        for file in files_to_check {
1085            let path = android_dir.join(file);
1086            let contents = fs::read_to_string(&path).expect(&format!("Failed to read {}", file));
1087
1088            // Check for unreplaced placeholders
1089            let has_placeholder = contents.contains("{{") && contents.contains("}}");
1090            assert!(
1091                !has_placeholder,
1092                "File {} contains unreplaced template placeholders: {}",
1093                file,
1094                contents
1095            );
1096        }
1097
1098        // Verify specific substitutions were made
1099        let settings = fs::read_to_string(android_dir.join("settings.gradle")).unwrap();
1100        assert!(
1101            settings.contains("my-bench-project-android") || settings.contains("my_bench_project-android"),
1102            "settings.gradle should contain project name"
1103        );
1104
1105        let build_gradle = fs::read_to_string(android_dir.join("app/build.gradle")).unwrap();
1106        // Package name should be sanitized (no hyphens/underscores) for consistency with iOS
1107        assert!(
1108            build_gradle.contains("dev.world.mybenchproject"),
1109            "build.gradle should contain sanitized package name 'dev.world.mybenchproject'"
1110        );
1111
1112        let manifest = fs::read_to_string(android_dir.join("app/src/main/AndroidManifest.xml")).unwrap();
1113        assert!(
1114            manifest.contains("Theme.MyBenchProject"),
1115            "AndroidManifest.xml should contain PascalCase theme name"
1116        );
1117
1118        let strings = fs::read_to_string(android_dir.join("app/src/main/res/values/strings.xml")).unwrap();
1119        assert!(
1120            strings.contains("Benchmark"),
1121            "strings.xml should contain app name with Benchmark"
1122        );
1123
1124        // Verify Kotlin files are in the correct package directory structure
1125        // For package "dev.world.mybenchproject", files should be in "dev/world/mybenchproject/"
1126        let main_activity_path = android_dir.join("app/src/main/java/dev/world/mybenchproject/MainActivity.kt");
1127        assert!(
1128            main_activity_path.exists(),
1129            "MainActivity.kt should be in package directory: {:?}",
1130            main_activity_path
1131        );
1132
1133        let test_activity_path = android_dir.join("app/src/androidTest/java/dev/world/mybenchproject/MainActivityTest.kt");
1134        assert!(
1135            test_activity_path.exists(),
1136            "MainActivityTest.kt should be in package directory: {:?}",
1137            test_activity_path
1138        );
1139
1140        // Verify the files are NOT in the root java directory
1141        assert!(
1142            !android_dir.join("app/src/main/java/MainActivity.kt").exists(),
1143            "MainActivity.kt should not be in root java directory"
1144        );
1145        assert!(
1146            !android_dir.join("app/src/androidTest/java/MainActivityTest.kt").exists(),
1147            "MainActivityTest.kt should not be in root java directory"
1148        );
1149
1150        // Cleanup
1151        fs::remove_dir_all(&temp_dir).ok();
1152    }
1153
1154    #[test]
1155    fn test_is_template_file() {
1156        assert!(is_template_file(Path::new("settings.gradle")));
1157        assert!(is_template_file(Path::new("app/build.gradle")));
1158        assert!(is_template_file(Path::new("AndroidManifest.xml")));
1159        assert!(is_template_file(Path::new("strings.xml")));
1160        assert!(is_template_file(Path::new("MainActivity.kt.template")));
1161        assert!(is_template_file(Path::new("project.yml")));
1162        assert!(is_template_file(Path::new("Info.plist")));
1163        assert!(!is_template_file(Path::new("libfoo.so")));
1164        assert!(!is_template_file(Path::new("image.png")));
1165    }
1166
1167    #[test]
1168    fn test_validate_no_unreplaced_placeholders() {
1169        // Should pass with no placeholders
1170        assert!(validate_no_unreplaced_placeholders("hello world", Path::new("test.txt")).is_ok());
1171
1172        // Should pass with Gradle variables (not our placeholders)
1173        assert!(validate_no_unreplaced_placeholders("${ENV_VAR}", Path::new("test.txt")).is_ok());
1174
1175        // Should fail with unreplaced template placeholders
1176        let result = validate_no_unreplaced_placeholders("hello {{NAME}}", Path::new("test.txt"));
1177        assert!(result.is_err());
1178        let err = result.unwrap_err().to_string();
1179        assert!(err.contains("{{NAME}}"));
1180    }
1181
1182    #[test]
1183    fn test_to_pascal_case() {
1184        assert_eq!(to_pascal_case("my-project"), "MyProject");
1185        assert_eq!(to_pascal_case("my_project"), "MyProject");
1186        assert_eq!(to_pascal_case("myproject"), "Myproject");
1187        assert_eq!(to_pascal_case("my-bench-project"), "MyBenchProject");
1188    }
1189
1190    #[test]
1191    fn test_detect_default_function_finds_benchmark() {
1192        let temp_dir = env::temp_dir().join("mobench-sdk-detect-test");
1193        let _ = fs::remove_dir_all(&temp_dir);
1194        fs::create_dir_all(temp_dir.join("src")).unwrap();
1195
1196        // Create a lib.rs with a benchmark function
1197        let lib_content = r#"
1198use mobench_sdk::benchmark;
1199
1200/// Some docs
1201#[benchmark]
1202fn my_benchmark_func() {
1203    // benchmark code
1204}
1205
1206fn helper_func() {}
1207"#;
1208        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1209        fs::write(temp_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
1210
1211        let result = detect_default_function(&temp_dir, "my_crate");
1212        assert_eq!(result, Some("my_crate::my_benchmark_func".to_string()));
1213
1214        // Cleanup
1215        fs::remove_dir_all(&temp_dir).ok();
1216    }
1217
1218    #[test]
1219    fn test_detect_default_function_no_benchmark() {
1220        let temp_dir = env::temp_dir().join("mobench-sdk-detect-none-test");
1221        let _ = fs::remove_dir_all(&temp_dir);
1222        fs::create_dir_all(temp_dir.join("src")).unwrap();
1223
1224        // Create a lib.rs without benchmark functions
1225        let lib_content = r#"
1226fn regular_function() {
1227    // no benchmark here
1228}
1229"#;
1230        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1231
1232        let result = detect_default_function(&temp_dir, "my_crate");
1233        assert!(result.is_none());
1234
1235        // Cleanup
1236        fs::remove_dir_all(&temp_dir).ok();
1237    }
1238
1239    #[test]
1240    fn test_detect_default_function_pub_fn() {
1241        let temp_dir = env::temp_dir().join("mobench-sdk-detect-pub-test");
1242        let _ = fs::remove_dir_all(&temp_dir);
1243        fs::create_dir_all(temp_dir.join("src")).unwrap();
1244
1245        // Create a lib.rs with a public benchmark function
1246        let lib_content = r#"
1247#[benchmark]
1248pub fn public_bench() {
1249    // benchmark code
1250}
1251"#;
1252        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1253
1254        let result = detect_default_function(&temp_dir, "test-crate");
1255        assert_eq!(result, Some("test_crate::public_bench".to_string()));
1256
1257        // Cleanup
1258        fs::remove_dir_all(&temp_dir).ok();
1259    }
1260
1261    #[test]
1262    fn test_resolve_default_function_fallback() {
1263        let temp_dir = env::temp_dir().join("mobench-sdk-resolve-test");
1264        let _ = fs::remove_dir_all(&temp_dir);
1265        fs::create_dir_all(&temp_dir).unwrap();
1266
1267        // No lib.rs exists, should fall back to default
1268        let result = resolve_default_function(&temp_dir, "my-crate", None);
1269        assert_eq!(result, "my_crate::example_benchmark");
1270
1271        // Cleanup
1272        fs::remove_dir_all(&temp_dir).ok();
1273    }
1274
1275    #[test]
1276    fn test_sanitize_bundle_id_component() {
1277        // Hyphens should be removed
1278        assert_eq!(sanitize_bundle_id_component("bench-mobile"), "benchmobile");
1279        // Underscores should be removed
1280        assert_eq!(sanitize_bundle_id_component("bench_mobile"), "benchmobile");
1281        // Mixed separators should all be removed
1282        assert_eq!(sanitize_bundle_id_component("my-project_name"), "myprojectname");
1283        // Already valid should remain unchanged (but lowercase)
1284        assert_eq!(sanitize_bundle_id_component("benchmobile"), "benchmobile");
1285        // Numbers should be preserved
1286        assert_eq!(sanitize_bundle_id_component("bench2mobile"), "bench2mobile");
1287        // Uppercase should be lowercased
1288        assert_eq!(sanitize_bundle_id_component("BenchMobile"), "benchmobile");
1289        // Complex case
1290        assert_eq!(sanitize_bundle_id_component("My-Complex_Project-123"), "mycomplexproject123");
1291    }
1292
1293    #[test]
1294    fn test_generate_ios_project_bundle_id_not_duplicated() {
1295        let temp_dir = env::temp_dir().join("mobench-sdk-ios-bundle-test");
1296        // Clean up any previous test run
1297        let _ = fs::remove_dir_all(&temp_dir);
1298        fs::create_dir_all(&temp_dir).unwrap();
1299
1300        // Use a crate name that would previously cause duplication
1301        let crate_name = "bench-mobile";
1302        let bundle_prefix = "dev.world.benchmobile";
1303        let project_pascal = "BenchRunner";
1304
1305        let result = generate_ios_project(
1306            &temp_dir,
1307            crate_name,
1308            project_pascal,
1309            bundle_prefix,
1310            "bench_mobile::test_func",
1311        );
1312        assert!(result.is_ok(), "generate_ios_project failed: {:?}", result.err());
1313
1314        // Verify project.yml was created
1315        let project_yml_path = temp_dir.join("ios/BenchRunner/project.yml");
1316        assert!(project_yml_path.exists(), "project.yml should exist");
1317
1318        // Read and verify the bundle ID is correct (not duplicated)
1319        let project_yml = fs::read_to_string(&project_yml_path).unwrap();
1320
1321        // The bundle ID should be "dev.world.benchmobile.BenchRunner"
1322        // NOT "dev.world.benchmobile.benchmobile"
1323        assert!(
1324            project_yml.contains("dev.world.benchmobile.BenchRunner"),
1325            "Bundle ID should be 'dev.world.benchmobile.BenchRunner', got:\n{}",
1326            project_yml
1327        );
1328        assert!(
1329            !project_yml.contains("dev.world.benchmobile.benchmobile"),
1330            "Bundle ID should NOT be duplicated as 'dev.world.benchmobile.benchmobile', got:\n{}",
1331            project_yml
1332        );
1333
1334        // Cleanup
1335        fs::remove_dir_all(&temp_dir).ok();
1336    }
1337
1338    #[test]
1339    fn test_cross_platform_naming_consistency() {
1340        // Test that Android and iOS use the same naming convention for package/bundle IDs
1341        let temp_dir = env::temp_dir().join("mobench-sdk-naming-consistency-test");
1342        let _ = fs::remove_dir_all(&temp_dir);
1343        fs::create_dir_all(&temp_dir).unwrap();
1344
1345        let project_name = "bench-mobile";
1346
1347        // Generate Android project
1348        let result = generate_android_project(&temp_dir, project_name, "bench_mobile::test_func");
1349        assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
1350
1351        // Generate iOS project (mimicking how ensure_ios_project does it)
1352        let bundle_id_component = sanitize_bundle_id_component(project_name);
1353        let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1354        let result = generate_ios_project(
1355            &temp_dir,
1356            &project_name.replace('-', "_"),
1357            "BenchRunner",
1358            &bundle_prefix,
1359            "bench_mobile::test_func",
1360        );
1361        assert!(result.is_ok(), "generate_ios_project failed: {:?}", result.err());
1362
1363        // Read Android build.gradle to extract package name
1364        let android_build_gradle = fs::read_to_string(
1365            temp_dir.join("android/app/build.gradle")
1366        ).expect("Failed to read Android build.gradle");
1367
1368        // Read iOS project.yml to extract bundle ID prefix
1369        let ios_project_yml = fs::read_to_string(
1370            temp_dir.join("ios/BenchRunner/project.yml")
1371        ).expect("Failed to read iOS project.yml");
1372
1373        // Both should use "benchmobile" (without hyphens or underscores)
1374        // Android: namespace = "dev.world.benchmobile"
1375        // iOS: bundleIdPrefix: dev.world.benchmobile
1376        assert!(
1377            android_build_gradle.contains("dev.world.benchmobile"),
1378            "Android package should be 'dev.world.benchmobile', got:\n{}",
1379            android_build_gradle
1380        );
1381        assert!(
1382            ios_project_yml.contains("dev.world.benchmobile"),
1383            "iOS bundle prefix should contain 'dev.world.benchmobile', got:\n{}",
1384            ios_project_yml
1385        );
1386
1387        // Ensure Android doesn't use hyphens or underscores in the package ID component
1388        assert!(
1389            !android_build_gradle.contains("dev.world.bench-mobile"),
1390            "Android package should NOT contain hyphens"
1391        );
1392        assert!(
1393            !android_build_gradle.contains("dev.world.bench_mobile"),
1394            "Android package should NOT contain underscores"
1395        );
1396
1397        // Cleanup
1398        fs::remove_dir_all(&temp_dir).ok();
1399    }
1400
1401    #[test]
1402    fn test_cross_platform_version_consistency() {
1403        // Test that Android and iOS use the same version strings
1404        let temp_dir = env::temp_dir().join("mobench-sdk-version-consistency-test");
1405        let _ = fs::remove_dir_all(&temp_dir);
1406        fs::create_dir_all(&temp_dir).unwrap();
1407
1408        let project_name = "test-project";
1409
1410        // Generate Android project
1411        let result = generate_android_project(&temp_dir, project_name, "test_project::test_func");
1412        assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
1413
1414        // Generate iOS project
1415        let bundle_id_component = sanitize_bundle_id_component(project_name);
1416        let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1417        let result = generate_ios_project(
1418            &temp_dir,
1419            &project_name.replace('-', "_"),
1420            "BenchRunner",
1421            &bundle_prefix,
1422            "test_project::test_func",
1423        );
1424        assert!(result.is_ok(), "generate_ios_project failed: {:?}", result.err());
1425
1426        // Read Android build.gradle
1427        let android_build_gradle = fs::read_to_string(
1428            temp_dir.join("android/app/build.gradle")
1429        ).expect("Failed to read Android build.gradle");
1430
1431        // Read iOS project.yml
1432        let ios_project_yml = fs::read_to_string(
1433            temp_dir.join("ios/BenchRunner/project.yml")
1434        ).expect("Failed to read iOS project.yml");
1435
1436        // Both should use version "1.0.0"
1437        assert!(
1438            android_build_gradle.contains("versionName \"1.0.0\""),
1439            "Android versionName should be '1.0.0', got:\n{}",
1440            android_build_gradle
1441        );
1442        assert!(
1443            ios_project_yml.contains("CFBundleShortVersionString: \"1.0.0\""),
1444            "iOS CFBundleShortVersionString should be '1.0.0', got:\n{}",
1445            ios_project_yml
1446        );
1447
1448        // Cleanup
1449        fs::remove_dir_all(&temp_dir).ok();
1450    }
1451
1452    #[test]
1453    fn test_bundle_id_prefix_consistency() {
1454        // Test that the bundle ID prefix format is consistent across platforms
1455        let test_cases = vec![
1456            ("my-project", "dev.world.myproject"),
1457            ("bench_mobile", "dev.world.benchmobile"),
1458            ("TestApp", "dev.world.testapp"),
1459            ("app-with-many-dashes", "dev.world.appwithmanydashes"),
1460            ("app_with_many_underscores", "dev.world.appwithmanyunderscores"),
1461        ];
1462
1463        for (input, expected_prefix) in test_cases {
1464            let sanitized = sanitize_bundle_id_component(input);
1465            let full_prefix = format!("dev.world.{}", sanitized);
1466            assert_eq!(
1467                full_prefix, expected_prefix,
1468                "For input '{}', expected '{}' but got '{}'",
1469                input, expected_prefix, full_prefix
1470            );
1471        }
1472    }
1473}