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    let bundle_prefix = format!("dev.world.{}", project_slug);
44
45    // Create base directories
46    fs::create_dir_all(output_dir)?;
47
48    // Generate bench-mobile FFI wrapper crate
49    generate_bench_mobile_crate(output_dir, &project_slug)?;
50
51    // For full project generation (init), use "example_fibonacci" as the default
52    // since the generated example benchmarks include this function
53    let default_function = "example_fibonacci";
54
55    // Generate platform-specific projects
56    match config.target {
57        Target::Android => {
58            generate_android_project(output_dir, &project_slug, default_function)?;
59        }
60        Target::Ios => {
61            generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix, default_function)?;
62        }
63        Target::Both => {
64            generate_android_project(output_dir, &project_slug, default_function)?;
65            generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix, default_function)?;
66        }
67    }
68
69    // Generate config file
70    generate_config_file(output_dir, config)?;
71
72    // Generate examples if requested
73    if config.generate_examples {
74        generate_example_benchmarks(output_dir)?;
75    }
76
77    Ok(output_dir.clone())
78}
79
80/// Generates the bench-mobile FFI wrapper crate
81fn generate_bench_mobile_crate(output_dir: &Path, project_name: &str) -> Result<(), BenchError> {
82    let crate_dir = output_dir.join("bench-mobile");
83    fs::create_dir_all(crate_dir.join("src"))?;
84
85    let crate_name = format!("{}-bench-mobile", project_name);
86
87    // Generate Cargo.toml
88    // Note: We configure rustls to use 'ring' instead of 'aws-lc-rs' (default in rustls 0.23+)
89    // because aws-lc-rs doesn't compile for Android NDK targets.
90    let cargo_toml = format!(
91        r#"[package]
92name = "{}"
93version = "0.1.0"
94edition = "2021"
95
96[lib]
97crate-type = ["cdylib", "staticlib", "rlib"]
98
99[dependencies]
100mobench-sdk = {{ path = ".." }}
101uniffi = "0.28"
102{} = {{ path = ".." }}
103
104[features]
105default = []
106
107[build-dependencies]
108uniffi = {{ version = "0.28", features = ["build"] }}
109
110# Binary for generating UniFFI bindings (used by mobench build)
111[[bin]]
112name = "uniffi-bindgen"
113path = "src/bin/uniffi-bindgen.rs"
114
115# IMPORTANT: If your project uses rustls (directly or transitively), you must configure
116# it to use the 'ring' crypto backend instead of 'aws-lc-rs' (the default in rustls 0.23+).
117# aws-lc-rs doesn't compile for Android NDK targets due to C compilation issues.
118#
119# Add this to your root Cargo.toml:
120# [workspace.dependencies]
121# rustls = {{ version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }}
122#
123# Then in each crate that uses rustls:
124# [dependencies]
125# rustls = {{ workspace = true }}
126"#,
127        crate_name, project_name
128    );
129
130    fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
131
132    // Generate src/lib.rs
133    let lib_rs_template = r#"//! Mobile FFI bindings for benchmarks
134//!
135//! This crate provides the FFI boundary between Rust benchmarks and mobile
136//! platforms (Android/iOS). It uses UniFFI to generate type-safe bindings.
137
138use uniffi;
139
140// Ensure the user crate is linked so benchmark registrations are pulled in.
141extern crate {{USER_CRATE}} as _bench_user_crate;
142
143// Re-export mobench-sdk types with UniFFI annotations
144#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
145pub struct BenchSpec {
146    pub name: String,
147    pub iterations: u32,
148    pub warmup: u32,
149}
150
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
152pub struct BenchSample {
153    pub duration_ns: u64,
154}
155
156#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
157pub struct BenchReport {
158    pub spec: BenchSpec,
159    pub samples: Vec<BenchSample>,
160}
161
162#[derive(Debug, thiserror::Error, uniffi::Error)]
163#[uniffi(flat_error)]
164pub enum BenchError {
165    #[error("iterations must be greater than zero")]
166    InvalidIterations,
167
168    #[error("unknown benchmark function: {name}")]
169    UnknownFunction { name: String },
170
171    #[error("benchmark execution failed: {reason}")]
172    ExecutionFailed { reason: String },
173}
174
175// Convert from mobench-sdk types
176impl From<mobench_sdk::BenchSpec> for BenchSpec {
177    fn from(spec: mobench_sdk::BenchSpec) -> Self {
178        Self {
179            name: spec.name,
180            iterations: spec.iterations,
181            warmup: spec.warmup,
182        }
183    }
184}
185
186impl From<BenchSpec> for mobench_sdk::BenchSpec {
187    fn from(spec: BenchSpec) -> Self {
188        Self {
189            name: spec.name,
190            iterations: spec.iterations,
191            warmup: spec.warmup,
192        }
193    }
194}
195
196impl From<mobench_sdk::BenchSample> for BenchSample {
197    fn from(sample: mobench_sdk::BenchSample) -> Self {
198        Self {
199            duration_ns: sample.duration_ns,
200        }
201    }
202}
203
204impl From<mobench_sdk::RunnerReport> for BenchReport {
205    fn from(report: mobench_sdk::RunnerReport) -> Self {
206        Self {
207            spec: report.spec.into(),
208            samples: report.samples.into_iter().map(Into::into).collect(),
209        }
210    }
211}
212
213impl From<mobench_sdk::BenchError> for BenchError {
214    fn from(err: mobench_sdk::BenchError) -> Self {
215        match err {
216            mobench_sdk::BenchError::Runner(runner_err) => {
217                BenchError::ExecutionFailed {
218                    reason: runner_err.to_string(),
219                }
220            }
221            mobench_sdk::BenchError::UnknownFunction(name) => {
222                BenchError::UnknownFunction { name }
223            }
224            _ => BenchError::ExecutionFailed {
225                reason: err.to_string(),
226            },
227        }
228    }
229}
230
231/// Runs a benchmark by name with the given specification
232///
233/// This is the main FFI entry point called from mobile platforms.
234#[uniffi::export]
235pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
236    let sdk_spec: mobench_sdk::BenchSpec = spec.into();
237    let report = mobench_sdk::run_benchmark(sdk_spec)?;
238    Ok(report.into())
239}
240
241// Generate UniFFI scaffolding
242uniffi::setup_scaffolding!();
243"#;
244
245    let lib_rs = render_template(
246        lib_rs_template,
247        &[TemplateVar {
248            name: "USER_CRATE",
249            value: project_name.replace('-', "_"),
250        }],
251    );
252    fs::write(crate_dir.join("src/lib.rs"), lib_rs)?;
253
254    // Generate build.rs
255    let build_rs = r#"fn main() {
256    uniffi::generate_scaffolding("src/lib.rs").unwrap();
257}
258"#;
259
260    fs::write(crate_dir.join("build.rs"), build_rs)?;
261
262    // Generate uniffi-bindgen binary (used by mobench build)
263    let bin_dir = crate_dir.join("src/bin");
264    fs::create_dir_all(&bin_dir)?;
265    let uniffi_bindgen_rs = r#"fn main() {
266    uniffi::uniffi_bindgen_main()
267}
268"#;
269    fs::write(bin_dir.join("uniffi-bindgen.rs"), uniffi_bindgen_rs)?;
270
271    Ok(())
272}
273
274/// Generates Android project structure from templates
275///
276/// This function can be called standalone to generate just the Android
277/// project scaffolding, useful for auto-generation during build.
278///
279/// # Arguments
280///
281/// * `output_dir` - Directory to write the `android/` project into
282/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
283/// * `default_function` - Default benchmark function to use (e.g., "bench_mobile::my_benchmark")
284pub fn generate_android_project(
285    output_dir: &Path,
286    project_slug: &str,
287    default_function: &str,
288) -> Result<(), BenchError> {
289    let target_dir = output_dir.join("android");
290    let library_name = project_slug.replace('-', "_");
291    let project_pascal = to_pascal_case(project_slug);
292    let vars = vec![
293        TemplateVar {
294            name: "PROJECT_NAME",
295            value: project_slug.to_string(),
296        },
297        TemplateVar {
298            name: "PROJECT_NAME_PASCAL",
299            value: project_pascal.clone(),
300        },
301        TemplateVar {
302            name: "APP_NAME",
303            value: format!("{} Benchmark", project_pascal),
304        },
305        TemplateVar {
306            name: "PACKAGE_NAME",
307            value: format!("dev.world.{}", project_slug),
308        },
309        TemplateVar {
310            name: "UNIFFI_NAMESPACE",
311            value: library_name.clone(),
312        },
313        TemplateVar {
314            name: "LIBRARY_NAME",
315            value: library_name,
316        },
317        TemplateVar {
318            name: "DEFAULT_FUNCTION",
319            value: default_function.to_string(),
320        },
321    ];
322    render_dir(&ANDROID_TEMPLATES, &target_dir, &vars)?;
323    Ok(())
324}
325
326/// Generates iOS project structure from templates
327///
328/// This function can be called standalone to generate just the iOS
329/// project scaffolding, useful for auto-generation during build.
330///
331/// # Arguments
332///
333/// * `output_dir` - Directory to write the `ios/` project into
334/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
335/// * `project_pascal` - PascalCase version of project name (e.g., "BenchMobile")
336/// * `bundle_prefix` - iOS bundle ID prefix (e.g., "dev.world.bench")
337/// * `default_function` - Default benchmark function to use (e.g., "bench_mobile::my_benchmark")
338pub fn generate_ios_project(
339    output_dir: &Path,
340    project_slug: &str,
341    project_pascal: &str,
342    bundle_prefix: &str,
343    default_function: &str,
344) -> Result<(), BenchError> {
345    let target_dir = output_dir.join("ios");
346    let vars = vec![
347        TemplateVar {
348            name: "DEFAULT_FUNCTION",
349            value: default_function.to_string(),
350        },
351        TemplateVar {
352            name: "PROJECT_NAME_PASCAL",
353            value: project_pascal.to_string(),
354        },
355        TemplateVar {
356            name: "BUNDLE_ID_PREFIX",
357            value: bundle_prefix.to_string(),
358        },
359        TemplateVar {
360            name: "BUNDLE_ID",
361            value: format!("{}.{}", bundle_prefix, project_slug),
362        },
363        TemplateVar {
364            name: "LIBRARY_NAME",
365            value: project_slug.replace('-', "_"),
366        },
367    ];
368    render_dir(&IOS_TEMPLATES, &target_dir, &vars)?;
369    Ok(())
370}
371
372/// Generates bench-config.toml configuration file
373fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
374    let config_target = match config.target {
375        Target::Ios => "ios",
376        Target::Android | Target::Both => "android",
377    };
378    let config_content = format!(
379        r#"# mobench configuration
380# This file controls how benchmarks are executed on devices.
381
382target = "{}"
383function = "example_fibonacci"
384iterations = 100
385warmup = 10
386device_matrix = "device-matrix.yaml"
387device_tags = ["default"]
388
389[browserstack]
390app_automate_username = "${{BROWSERSTACK_USERNAME}}"
391app_automate_access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
392project = "{}-benchmarks"
393
394[ios_xcuitest]
395app = "target/ios/BenchRunner.ipa"
396test_suite = "target/ios/BenchRunnerUITests.zip"
397"#,
398        config_target, config.project_name
399    );
400
401    fs::write(output_dir.join("bench-config.toml"), config_content)?;
402
403    Ok(())
404}
405
406/// Generates example benchmark functions
407fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
408    let examples_dir = output_dir.join("benches");
409    fs::create_dir_all(&examples_dir)?;
410
411    let example_content = r#"//! Example benchmarks
412//!
413//! This file demonstrates how to write benchmarks with mobench-sdk.
414
415use mobench_sdk::benchmark;
416
417/// Simple benchmark example
418#[benchmark]
419fn example_fibonacci() {
420    let result = fibonacci(30);
421    std::hint::black_box(result);
422}
423
424/// Another example with a loop
425#[benchmark]
426fn example_sum() {
427    let mut sum = 0u64;
428    for i in 0..10000 {
429        sum = sum.wrapping_add(i);
430    }
431    std::hint::black_box(sum);
432}
433
434// Helper function (not benchmarked)
435fn fibonacci(n: u32) -> u64 {
436    match n {
437        0 => 0,
438        1 => 1,
439        _ => {
440            let mut a = 0u64;
441            let mut b = 1u64;
442            for _ in 2..=n {
443                let next = a.wrapping_add(b);
444                a = b;
445                b = next;
446            }
447            b
448        }
449    }
450}
451"#;
452
453    fs::write(examples_dir.join("example.rs"), example_content)?;
454
455    Ok(())
456}
457
458/// File extensions that should be processed for template variable substitution
459const TEMPLATE_EXTENSIONS: &[&str] = &[
460    "gradle", "xml", "kt", "java", "swift", "yml", "yaml", "json", "toml", "md", "txt", "h", "m",
461    "plist", "pbxproj", "xcscheme", "xcworkspacedata", "entitlements", "modulemap",
462];
463
464fn render_dir(
465    dir: &Dir,
466    out_root: &Path,
467    vars: &[TemplateVar],
468) -> Result<(), BenchError> {
469    for entry in dir.entries() {
470        match entry {
471            DirEntry::Dir(sub) => {
472                // Skip cache directories
473                if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
474                    continue;
475                }
476                render_dir(sub, out_root, vars)?;
477            }
478            DirEntry::File(file) => {
479                if file.path().components().any(|c| c.as_os_str() == ".gradle") {
480                    continue;
481                }
482                // file.path() returns the full relative path from the embedded dir root
483                let mut relative = file.path().to_path_buf();
484                let mut contents = file.contents().to_vec();
485
486                // Check if file has .template extension (explicit template)
487                let is_explicit_template = relative
488                    .extension()
489                    .map(|ext| ext == "template")
490                    .unwrap_or(false);
491
492                // Check if file is a text file that should be processed for templates
493                let should_render = is_explicit_template || is_template_file(&relative);
494
495                if is_explicit_template {
496                    // Remove .template extension from output filename
497                    relative.set_extension("");
498                }
499
500                if should_render {
501                    if let Ok(text) = std::str::from_utf8(&contents) {
502                        let rendered = render_template(text, vars);
503                        // Validate that all template variables were replaced
504                        validate_no_unreplaced_placeholders(&rendered, &relative)?;
505                        contents = rendered.into_bytes();
506                    }
507                }
508
509                let out_path = out_root.join(relative);
510                if let Some(parent) = out_path.parent() {
511                    fs::create_dir_all(parent)?;
512                }
513                fs::write(&out_path, contents)?;
514            }
515        }
516    }
517    Ok(())
518}
519
520/// Checks if a file should be processed for template variable substitution
521/// based on its extension
522fn is_template_file(path: &Path) -> bool {
523    // Check for .template extension on any file
524    if let Some(ext) = path.extension() {
525        if ext == "template" {
526            return true;
527        }
528        // Check if the base extension is in our list
529        if let Some(ext_str) = ext.to_str() {
530            return TEMPLATE_EXTENSIONS.contains(&ext_str);
531        }
532    }
533    // Also check the filename without the .template extension
534    if let Some(stem) = path.file_stem() {
535        let stem_path = Path::new(stem);
536        if let Some(ext) = stem_path.extension() {
537            if let Some(ext_str) = ext.to_str() {
538                return TEMPLATE_EXTENSIONS.contains(&ext_str);
539            }
540        }
541    }
542    false
543}
544
545/// Validates that no unreplaced template placeholders remain in the rendered content
546fn validate_no_unreplaced_placeholders(content: &str, file_path: &Path) -> Result<(), BenchError> {
547    // Find all {{...}} patterns
548    let mut pos = 0;
549    let mut unreplaced = Vec::new();
550
551    while let Some(start) = content[pos..].find("{{") {
552        let abs_start = pos + start;
553        if let Some(end) = content[abs_start..].find("}}") {
554            let placeholder = &content[abs_start..abs_start + end + 2];
555            // Extract just the variable name
556            let var_name = &content[abs_start + 2..abs_start + end];
557            // Skip placeholders that look like Gradle variable syntax (e.g., ${...})
558            // or other non-template patterns
559            if !var_name.contains('$') && !var_name.contains(' ') && !var_name.is_empty() {
560                unreplaced.push(placeholder.to_string());
561            }
562            pos = abs_start + end + 2;
563        } else {
564            break;
565        }
566    }
567
568    if !unreplaced.is_empty() {
569        return Err(BenchError::Build(format!(
570            "Template validation failed for {:?}: unreplaced placeholders found: {:?}\n\n\
571             This is a bug in mobench-sdk. Please report it at:\n\
572             https://github.com/worldcoin/mobile-bench-rs/issues",
573            file_path, unreplaced
574        )));
575    }
576
577    Ok(())
578}
579
580fn render_template(input: &str, vars: &[TemplateVar]) -> String {
581    let mut output = input.to_string();
582    for var in vars {
583        output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
584    }
585    output
586}
587
588fn sanitize_package_name(name: &str) -> String {
589    name.chars()
590        .map(|c| {
591            if c.is_ascii_alphanumeric() {
592                c.to_ascii_lowercase()
593            } else {
594                '-'
595            }
596        })
597        .collect::<String>()
598        .trim_matches('-')
599        .replace("--", "-")
600}
601
602/// Converts a string to PascalCase
603pub fn to_pascal_case(input: &str) -> String {
604    input
605        .split(|c: char| !c.is_ascii_alphanumeric())
606        .filter(|s| !s.is_empty())
607        .map(|s| {
608            let mut chars = s.chars();
609            let first = chars.next().unwrap().to_ascii_uppercase();
610            let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
611            format!("{}{}", first, rest)
612        })
613        .collect::<String>()
614}
615
616/// Checks if the Android project scaffolding exists at the given output directory
617///
618/// Returns true if the `android/build.gradle` or `android/build.gradle.kts` file exists.
619pub fn android_project_exists(output_dir: &Path) -> bool {
620    let android_dir = output_dir.join("android");
621    android_dir.join("build.gradle").exists() || android_dir.join("build.gradle.kts").exists()
622}
623
624/// Checks if the iOS project scaffolding exists at the given output directory
625///
626/// Returns true if the `ios/BenchRunner/project.yml` file exists.
627pub fn ios_project_exists(output_dir: &Path) -> bool {
628    output_dir.join("ios/BenchRunner/project.yml").exists()
629}
630
631/// Detects the first benchmark function in a crate by scanning src/lib.rs for `#[benchmark]`
632///
633/// This function looks for functions marked with the `#[benchmark]` attribute and returns
634/// the first one found in the format `{crate_name}::{function_name}`.
635///
636/// # Arguments
637///
638/// * `crate_dir` - Path to the crate directory containing Cargo.toml
639/// * `crate_name` - Name of the crate (used as prefix for the function name)
640///
641/// # Returns
642///
643/// * `Some(String)` - The detected function name in format `crate_name::function_name`
644/// * `None` - If no benchmark functions are found or if the file cannot be read
645pub fn detect_default_function(crate_dir: &Path, crate_name: &str) -> Option<String> {
646    let lib_rs = crate_dir.join("src/lib.rs");
647    if !lib_rs.exists() {
648        return None;
649    }
650
651    let file = fs::File::open(&lib_rs).ok()?;
652    let reader = BufReader::new(file);
653
654    let mut found_benchmark_attr = false;
655    let crate_name_normalized = crate_name.replace('-', "_");
656
657    for line in reader.lines().map_while(Result::ok) {
658        let trimmed = line.trim();
659
660        // Check for #[benchmark] attribute
661        if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
662            found_benchmark_attr = true;
663            continue;
664        }
665
666        // If we found a benchmark attribute, look for the function definition
667        if found_benchmark_attr {
668            // Look for "fn function_name" or "pub fn function_name"
669            if let Some(fn_pos) = trimmed.find("fn ") {
670                let after_fn = &trimmed[fn_pos + 3..];
671                // Extract function name (until '(' or whitespace)
672                let fn_name: String = after_fn
673                    .chars()
674                    .take_while(|c| c.is_alphanumeric() || *c == '_')
675                    .collect();
676
677                if !fn_name.is_empty() {
678                    return Some(format!("{}::{}", crate_name_normalized, fn_name));
679                }
680            }
681            // Reset if we hit a line that's not a function definition
682            // (could be another attribute or comment)
683            if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
684                found_benchmark_attr = false;
685            }
686        }
687    }
688
689    None
690}
691
692/// Resolves the default benchmark function for a project
693///
694/// This function attempts to auto-detect benchmark functions from the crate's source.
695/// If no benchmarks are found, it falls back to a sensible default based on the crate name.
696///
697/// # Arguments
698///
699/// * `project_root` - Root directory of the project
700/// * `crate_name` - Name of the benchmark crate
701/// * `crate_dir` - Optional explicit crate directory (if None, will search standard locations)
702///
703/// # Returns
704///
705/// The default function name in format `crate_name::function_name`
706pub fn resolve_default_function(
707    project_root: &Path,
708    crate_name: &str,
709    crate_dir: Option<&Path>,
710) -> String {
711    let crate_name_normalized = crate_name.replace('-', "_");
712
713    // Try to find the crate directory
714    let search_dirs: Vec<PathBuf> = if let Some(dir) = crate_dir {
715        vec![dir.to_path_buf()]
716    } else {
717        vec![
718            project_root.join("bench-mobile"),
719            project_root.join("crates").join(crate_name),
720            project_root.to_path_buf(),
721        ]
722    };
723
724    // Try to detect benchmarks from each potential location
725    for dir in &search_dirs {
726        if dir.join("Cargo.toml").exists() {
727            if let Some(detected) = detect_default_function(dir, &crate_name_normalized) {
728                return detected;
729            }
730        }
731    }
732
733    // Fallback: use a sensible default based on crate name
734    format!("{}::example_benchmark", crate_name_normalized)
735}
736
737/// Auto-generates Android project scaffolding from a crate name
738///
739/// This is a convenience function that derives template variables from the
740/// crate name and generates the Android project structure. It auto-detects
741/// the default benchmark function from the crate's source code.
742///
743/// # Arguments
744///
745/// * `output_dir` - Directory to write the `android/` project into
746/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
747pub fn ensure_android_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
748    ensure_android_project_with_options(output_dir, crate_name, None, None)
749}
750
751/// Auto-generates Android project scaffolding with additional options
752///
753/// This is a more flexible version of `ensure_android_project` that allows
754/// specifying a custom default function and/or crate directory.
755///
756/// # Arguments
757///
758/// * `output_dir` - Directory to write the `android/` project into
759/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
760/// * `project_root` - Optional project root for auto-detecting benchmarks (defaults to output_dir parent)
761/// * `crate_dir` - Optional explicit crate directory for benchmark detection
762pub fn ensure_android_project_with_options(
763    output_dir: &Path,
764    crate_name: &str,
765    project_root: Option<&Path>,
766    crate_dir: Option<&Path>,
767) -> Result<(), BenchError> {
768    if android_project_exists(output_dir) {
769        return Ok(());
770    }
771
772    println!("Android project not found, generating scaffolding...");
773    let project_slug = crate_name.replace('-', "_");
774
775    // Resolve the default function by auto-detecting from source
776    let effective_root = project_root.unwrap_or_else(|| {
777        output_dir.parent().unwrap_or(output_dir)
778    });
779    let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
780
781    generate_android_project(output_dir, &project_slug, &default_function)?;
782    println!("  Generated Android project at {:?}", output_dir.join("android"));
783    println!("  Default benchmark function: {}", default_function);
784    Ok(())
785}
786
787/// Auto-generates iOS project scaffolding from a crate name
788///
789/// This is a convenience function that derives template variables from the
790/// crate name and generates the iOS project structure. It auto-detects
791/// the default benchmark function from the crate's source code.
792///
793/// # Arguments
794///
795/// * `output_dir` - Directory to write the `ios/` project into
796/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
797pub fn ensure_ios_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
798    ensure_ios_project_with_options(output_dir, crate_name, None, None)
799}
800
801/// Auto-generates iOS project scaffolding with additional options
802///
803/// This is a more flexible version of `ensure_ios_project` that allows
804/// specifying a custom default function and/or crate directory.
805///
806/// # Arguments
807///
808/// * `output_dir` - Directory to write the `ios/` project into
809/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
810/// * `project_root` - Optional project root for auto-detecting benchmarks (defaults to output_dir parent)
811/// * `crate_dir` - Optional explicit crate directory for benchmark detection
812pub fn ensure_ios_project_with_options(
813    output_dir: &Path,
814    crate_name: &str,
815    project_root: Option<&Path>,
816    crate_dir: Option<&Path>,
817) -> Result<(), BenchError> {
818    if ios_project_exists(output_dir) {
819        return Ok(());
820    }
821
822    println!("iOS project not found, generating scaffolding...");
823    // Use fixed "BenchRunner" for project/scheme name to match template directory structure
824    let project_pascal = "BenchRunner";
825    // Derive library name and bundle prefix from crate name
826    let library_name = crate_name.replace('-', "_");
827    let bundle_prefix = format!("dev.world.{}", library_name.replace('_', "-"));
828
829    // Resolve the default function by auto-detecting from source
830    let effective_root = project_root.unwrap_or_else(|| {
831        output_dir.parent().unwrap_or(output_dir)
832    });
833    let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
834
835    generate_ios_project(output_dir, &library_name, project_pascal, &bundle_prefix, &default_function)?;
836    println!("  Generated iOS project at {:?}", output_dir.join("ios"));
837    println!("  Default benchmark function: {}", default_function);
838    Ok(())
839}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844    use std::env;
845
846    #[test]
847    fn test_generate_bench_mobile_crate() {
848        let temp_dir = env::temp_dir().join("mobench-sdk-test");
849        fs::create_dir_all(&temp_dir).unwrap();
850
851        let result = generate_bench_mobile_crate(&temp_dir, "test_project");
852        assert!(result.is_ok());
853
854        // Verify files were created
855        assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
856        assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
857        assert!(temp_dir.join("bench-mobile/build.rs").exists());
858
859        // Cleanup
860        fs::remove_dir_all(&temp_dir).ok();
861    }
862
863    #[test]
864    fn test_generate_android_project_no_unreplaced_placeholders() {
865        let temp_dir = env::temp_dir().join("mobench-sdk-android-test");
866        // Clean up any previous test run
867        let _ = fs::remove_dir_all(&temp_dir);
868        fs::create_dir_all(&temp_dir).unwrap();
869
870        let result = generate_android_project(&temp_dir, "my-bench-project", "my_bench_project::test_func");
871        assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
872
873        // Verify key files exist
874        let android_dir = temp_dir.join("android");
875        assert!(android_dir.join("settings.gradle").exists());
876        assert!(android_dir.join("app/build.gradle").exists());
877        assert!(android_dir.join("app/src/main/AndroidManifest.xml").exists());
878        assert!(android_dir.join("app/src/main/res/values/strings.xml").exists());
879        assert!(android_dir.join("app/src/main/res/values/themes.xml").exists());
880
881        // Verify no unreplaced placeholders remain in generated files
882        let files_to_check = [
883            "settings.gradle",
884            "app/build.gradle",
885            "app/src/main/AndroidManifest.xml",
886            "app/src/main/res/values/strings.xml",
887            "app/src/main/res/values/themes.xml",
888        ];
889
890        for file in files_to_check {
891            let path = android_dir.join(file);
892            let contents = fs::read_to_string(&path).expect(&format!("Failed to read {}", file));
893
894            // Check for unreplaced placeholders
895            let has_placeholder = contents.contains("{{") && contents.contains("}}");
896            assert!(
897                !has_placeholder,
898                "File {} contains unreplaced template placeholders: {}",
899                file,
900                contents
901            );
902        }
903
904        // Verify specific substitutions were made
905        let settings = fs::read_to_string(android_dir.join("settings.gradle")).unwrap();
906        assert!(
907            settings.contains("my-bench-project-android") || settings.contains("my_bench_project-android"),
908            "settings.gradle should contain project name"
909        );
910
911        let build_gradle = fs::read_to_string(android_dir.join("app/build.gradle")).unwrap();
912        assert!(
913            build_gradle.contains("dev.world.my-bench-project") || build_gradle.contains("dev.world.my_bench_project"),
914            "build.gradle should contain package name"
915        );
916
917        let manifest = fs::read_to_string(android_dir.join("app/src/main/AndroidManifest.xml")).unwrap();
918        assert!(
919            manifest.contains("Theme.MyBenchProject"),
920            "AndroidManifest.xml should contain PascalCase theme name"
921        );
922
923        let strings = fs::read_to_string(android_dir.join("app/src/main/res/values/strings.xml")).unwrap();
924        assert!(
925            strings.contains("Benchmark"),
926            "strings.xml should contain app name with Benchmark"
927        );
928
929        // Cleanup
930        fs::remove_dir_all(&temp_dir).ok();
931    }
932
933    #[test]
934    fn test_is_template_file() {
935        assert!(is_template_file(Path::new("settings.gradle")));
936        assert!(is_template_file(Path::new("app/build.gradle")));
937        assert!(is_template_file(Path::new("AndroidManifest.xml")));
938        assert!(is_template_file(Path::new("strings.xml")));
939        assert!(is_template_file(Path::new("MainActivity.kt.template")));
940        assert!(is_template_file(Path::new("project.yml")));
941        assert!(is_template_file(Path::new("Info.plist")));
942        assert!(!is_template_file(Path::new("libfoo.so")));
943        assert!(!is_template_file(Path::new("image.png")));
944    }
945
946    #[test]
947    fn test_validate_no_unreplaced_placeholders() {
948        // Should pass with no placeholders
949        assert!(validate_no_unreplaced_placeholders("hello world", Path::new("test.txt")).is_ok());
950
951        // Should pass with Gradle variables (not our placeholders)
952        assert!(validate_no_unreplaced_placeholders("${ENV_VAR}", Path::new("test.txt")).is_ok());
953
954        // Should fail with unreplaced template placeholders
955        let result = validate_no_unreplaced_placeholders("hello {{NAME}}", Path::new("test.txt"));
956        assert!(result.is_err());
957        let err = result.unwrap_err().to_string();
958        assert!(err.contains("{{NAME}}"));
959    }
960
961    #[test]
962    fn test_to_pascal_case() {
963        assert_eq!(to_pascal_case("my-project"), "MyProject");
964        assert_eq!(to_pascal_case("my_project"), "MyProject");
965        assert_eq!(to_pascal_case("myproject"), "Myproject");
966        assert_eq!(to_pascal_case("my-bench-project"), "MyBenchProject");
967    }
968
969    #[test]
970    fn test_detect_default_function_finds_benchmark() {
971        let temp_dir = env::temp_dir().join("mobench-sdk-detect-test");
972        let _ = fs::remove_dir_all(&temp_dir);
973        fs::create_dir_all(temp_dir.join("src")).unwrap();
974
975        // Create a lib.rs with a benchmark function
976        let lib_content = r#"
977use mobench_sdk::benchmark;
978
979/// Some docs
980#[benchmark]
981fn my_benchmark_func() {
982    // benchmark code
983}
984
985fn helper_func() {}
986"#;
987        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
988        fs::write(temp_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
989
990        let result = detect_default_function(&temp_dir, "my_crate");
991        assert_eq!(result, Some("my_crate::my_benchmark_func".to_string()));
992
993        // Cleanup
994        fs::remove_dir_all(&temp_dir).ok();
995    }
996
997    #[test]
998    fn test_detect_default_function_no_benchmark() {
999        let temp_dir = env::temp_dir().join("mobench-sdk-detect-none-test");
1000        let _ = fs::remove_dir_all(&temp_dir);
1001        fs::create_dir_all(temp_dir.join("src")).unwrap();
1002
1003        // Create a lib.rs without benchmark functions
1004        let lib_content = r#"
1005fn regular_function() {
1006    // no benchmark here
1007}
1008"#;
1009        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1010
1011        let result = detect_default_function(&temp_dir, "my_crate");
1012        assert!(result.is_none());
1013
1014        // Cleanup
1015        fs::remove_dir_all(&temp_dir).ok();
1016    }
1017
1018    #[test]
1019    fn test_detect_default_function_pub_fn() {
1020        let temp_dir = env::temp_dir().join("mobench-sdk-detect-pub-test");
1021        let _ = fs::remove_dir_all(&temp_dir);
1022        fs::create_dir_all(temp_dir.join("src")).unwrap();
1023
1024        // Create a lib.rs with a public benchmark function
1025        let lib_content = r#"
1026#[benchmark]
1027pub fn public_bench() {
1028    // benchmark code
1029}
1030"#;
1031        fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1032
1033        let result = detect_default_function(&temp_dir, "test-crate");
1034        assert_eq!(result, Some("test_crate::public_bench".to_string()));
1035
1036        // Cleanup
1037        fs::remove_dir_all(&temp_dir).ok();
1038    }
1039
1040    #[test]
1041    fn test_resolve_default_function_fallback() {
1042        let temp_dir = env::temp_dir().join("mobench-sdk-resolve-test");
1043        let _ = fs::remove_dir_all(&temp_dir);
1044        fs::create_dir_all(&temp_dir).unwrap();
1045
1046        // No lib.rs exists, should fall back to default
1047        let result = resolve_default_function(&temp_dir, "my-crate", None);
1048        assert_eq!(result, "my_crate::example_benchmark");
1049
1050        // Cleanup
1051        fs::remove_dir_all(&temp_dir).ok();
1052    }
1053}