1use 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#[derive(Debug, Clone)]
18pub struct TemplateVar {
19 pub name: &'static str,
20 pub value: String,
21}
22
23pub 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_id_component = sanitize_bundle_id_component(&project_slug);
45 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
46
47 fs::create_dir_all(output_dir)?;
49
50 generate_bench_mobile_crate(output_dir, &project_slug)?;
52
53 let default_function = "example_fibonacci";
56
57 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(output_dir, config)?;
73
74 if config.generate_examples {
76 generate_example_benchmarks(output_dir)?;
77 }
78
79 Ok(output_dir.clone())
80}
81
82fn 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 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 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) => {
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 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 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
276pub 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 let vars = vec![
295 TemplateVar {
296 name: "PROJECT_NAME",
297 value: project_slug.to_string(),
298 },
299 TemplateVar {
300 name: "PROJECT_NAME_PASCAL",
301 value: project_pascal.clone(),
302 },
303 TemplateVar {
304 name: "APP_NAME",
305 value: format!("{} Benchmark", project_pascal),
306 },
307 TemplateVar {
308 name: "PACKAGE_NAME",
309 value: format!("dev.world.{}", project_slug),
310 },
311 TemplateVar {
312 name: "UNIFFI_NAMESPACE",
313 value: library_name.clone(),
314 },
315 TemplateVar {
316 name: "LIBRARY_NAME",
317 value: library_name,
318 },
319 TemplateVar {
320 name: "DEFAULT_FUNCTION",
321 value: default_function.to_string(),
322 },
323 ];
324 render_dir(&ANDROID_TEMPLATES, &target_dir, &vars)?;
325 Ok(())
326}
327
328pub fn generate_ios_project(
341 output_dir: &Path,
342 project_slug: &str,
343 project_pascal: &str,
344 bundle_prefix: &str,
345 default_function: &str,
346) -> Result<(), BenchError> {
347 let target_dir = output_dir.join("ios");
348 let sanitized_bundle_prefix = {
351 let parts: Vec<&str> = bundle_prefix.split('.').collect();
352 parts.iter()
353 .map(|part| sanitize_bundle_id_component(part))
354 .collect::<Vec<_>>()
355 .join(".")
356 };
357 let sanitized_project_slug = sanitize_bundle_id_component(project_slug);
358 let vars = vec![
359 TemplateVar {
360 name: "DEFAULT_FUNCTION",
361 value: default_function.to_string(),
362 },
363 TemplateVar {
364 name: "PROJECT_NAME_PASCAL",
365 value: project_pascal.to_string(),
366 },
367 TemplateVar {
368 name: "BUNDLE_ID_PREFIX",
369 value: sanitized_bundle_prefix.clone(),
370 },
371 TemplateVar {
372 name: "BUNDLE_ID",
373 value: format!("{}.{}", sanitized_bundle_prefix, sanitized_project_slug),
374 },
375 TemplateVar {
376 name: "LIBRARY_NAME",
377 value: project_slug.replace('-', "_"),
378 },
379 ];
380 render_dir(&IOS_TEMPLATES, &target_dir, &vars)?;
381 Ok(())
382}
383
384fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
386 let config_target = match config.target {
387 Target::Ios => "ios",
388 Target::Android | Target::Both => "android",
389 };
390 let config_content = format!(
391 r#"# mobench configuration
392# This file controls how benchmarks are executed on devices.
393
394target = "{}"
395function = "example_fibonacci"
396iterations = 100
397warmup = 10
398device_matrix = "device-matrix.yaml"
399device_tags = ["default"]
400
401[browserstack]
402app_automate_username = "${{BROWSERSTACK_USERNAME}}"
403app_automate_access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
404project = "{}-benchmarks"
405
406[ios_xcuitest]
407app = "target/ios/BenchRunner.ipa"
408test_suite = "target/ios/BenchRunnerUITests.zip"
409"#,
410 config_target, config.project_name
411 );
412
413 fs::write(output_dir.join("bench-config.toml"), config_content)?;
414
415 Ok(())
416}
417
418fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
420 let examples_dir = output_dir.join("benches");
421 fs::create_dir_all(&examples_dir)?;
422
423 let example_content = r#"//! Example benchmarks
424//!
425//! This file demonstrates how to write benchmarks with mobench-sdk.
426
427use mobench_sdk::benchmark;
428
429/// Simple benchmark example
430#[benchmark]
431fn example_fibonacci() {
432 let result = fibonacci(30);
433 std::hint::black_box(result);
434}
435
436/// Another example with a loop
437#[benchmark]
438fn example_sum() {
439 let mut sum = 0u64;
440 for i in 0..10000 {
441 sum = sum.wrapping_add(i);
442 }
443 std::hint::black_box(sum);
444}
445
446// Helper function (not benchmarked)
447fn fibonacci(n: u32) -> u64 {
448 match n {
449 0 => 0,
450 1 => 1,
451 _ => {
452 let mut a = 0u64;
453 let mut b = 1u64;
454 for _ in 2..=n {
455 let next = a.wrapping_add(b);
456 a = b;
457 b = next;
458 }
459 b
460 }
461 }
462}
463"#;
464
465 fs::write(examples_dir.join("example.rs"), example_content)?;
466
467 Ok(())
468}
469
470const TEMPLATE_EXTENSIONS: &[&str] = &[
472 "gradle", "xml", "kt", "java", "swift", "yml", "yaml", "json", "toml", "md", "txt", "h", "m",
473 "plist", "pbxproj", "xcscheme", "xcworkspacedata", "entitlements", "modulemap",
474];
475
476fn render_dir(
477 dir: &Dir,
478 out_root: &Path,
479 vars: &[TemplateVar],
480) -> Result<(), BenchError> {
481 for entry in dir.entries() {
482 match entry {
483 DirEntry::Dir(sub) => {
484 if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
486 continue;
487 }
488 render_dir(sub, out_root, vars)?;
489 }
490 DirEntry::File(file) => {
491 if file.path().components().any(|c| c.as_os_str() == ".gradle") {
492 continue;
493 }
494 let mut relative = file.path().to_path_buf();
496 let mut contents = file.contents().to_vec();
497
498 let is_explicit_template = relative
500 .extension()
501 .map(|ext| ext == "template")
502 .unwrap_or(false);
503
504 let should_render = is_explicit_template || is_template_file(&relative);
506
507 if is_explicit_template {
508 relative.set_extension("");
510 }
511
512 if should_render {
513 if let Ok(text) = std::str::from_utf8(&contents) {
514 let rendered = render_template(text, vars);
515 validate_no_unreplaced_placeholders(&rendered, &relative)?;
517 contents = rendered.into_bytes();
518 }
519 }
520
521 let out_path = out_root.join(relative);
522 if let Some(parent) = out_path.parent() {
523 fs::create_dir_all(parent)?;
524 }
525 fs::write(&out_path, contents)?;
526 }
527 }
528 }
529 Ok(())
530}
531
532fn is_template_file(path: &Path) -> bool {
535 if let Some(ext) = path.extension() {
537 if ext == "template" {
538 return true;
539 }
540 if let Some(ext_str) = ext.to_str() {
542 return TEMPLATE_EXTENSIONS.contains(&ext_str);
543 }
544 }
545 if let Some(stem) = path.file_stem() {
547 let stem_path = Path::new(stem);
548 if let Some(ext) = stem_path.extension() {
549 if let Some(ext_str) = ext.to_str() {
550 return TEMPLATE_EXTENSIONS.contains(&ext_str);
551 }
552 }
553 }
554 false
555}
556
557fn validate_no_unreplaced_placeholders(content: &str, file_path: &Path) -> Result<(), BenchError> {
559 let mut pos = 0;
561 let mut unreplaced = Vec::new();
562
563 while let Some(start) = content[pos..].find("{{") {
564 let abs_start = pos + start;
565 if let Some(end) = content[abs_start..].find("}}") {
566 let placeholder = &content[abs_start..abs_start + end + 2];
567 let var_name = &content[abs_start + 2..abs_start + end];
569 if !var_name.contains('$') && !var_name.contains(' ') && !var_name.is_empty() {
572 unreplaced.push(placeholder.to_string());
573 }
574 pos = abs_start + end + 2;
575 } else {
576 break;
577 }
578 }
579
580 if !unreplaced.is_empty() {
581 return Err(BenchError::Build(format!(
582 "Template validation failed for {:?}: unreplaced placeholders found: {:?}\n\n\
583 This is a bug in mobench-sdk. Please report it at:\n\
584 https://github.com/worldcoin/mobile-bench-rs/issues",
585 file_path, unreplaced
586 )));
587 }
588
589 Ok(())
590}
591
592fn render_template(input: &str, vars: &[TemplateVar]) -> String {
593 let mut output = input.to_string();
594 for var in vars {
595 output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
596 }
597 output
598}
599
600pub fn sanitize_bundle_id_component(name: &str) -> String {
611 name.chars()
612 .filter(|c| c.is_ascii_alphanumeric())
613 .collect::<String>()
614 .to_lowercase()
615}
616
617fn sanitize_package_name(name: &str) -> String {
618 name.chars()
619 .map(|c| {
620 if c.is_ascii_alphanumeric() {
621 c.to_ascii_lowercase()
622 } else {
623 '-'
624 }
625 })
626 .collect::<String>()
627 .trim_matches('-')
628 .replace("--", "-")
629}
630
631pub fn to_pascal_case(input: &str) -> String {
633 input
634 .split(|c: char| !c.is_ascii_alphanumeric())
635 .filter(|s| !s.is_empty())
636 .map(|s| {
637 let mut chars = s.chars();
638 let first = chars.next().unwrap().to_ascii_uppercase();
639 let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
640 format!("{}{}", first, rest)
641 })
642 .collect::<String>()
643}
644
645pub fn android_project_exists(output_dir: &Path) -> bool {
649 let android_dir = output_dir.join("android");
650 android_dir.join("build.gradle").exists() || android_dir.join("build.gradle.kts").exists()
651}
652
653pub fn ios_project_exists(output_dir: &Path) -> bool {
657 output_dir.join("ios/BenchRunner/project.yml").exists()
658}
659
660pub fn detect_default_function(crate_dir: &Path, crate_name: &str) -> Option<String> {
675 let lib_rs = crate_dir.join("src/lib.rs");
676 if !lib_rs.exists() {
677 return None;
678 }
679
680 let file = fs::File::open(&lib_rs).ok()?;
681 let reader = BufReader::new(file);
682
683 let mut found_benchmark_attr = false;
684 let crate_name_normalized = crate_name.replace('-', "_");
685
686 for line in reader.lines().map_while(Result::ok) {
687 let trimmed = line.trim();
688
689 if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
691 found_benchmark_attr = true;
692 continue;
693 }
694
695 if found_benchmark_attr {
697 if let Some(fn_pos) = trimmed.find("fn ") {
699 let after_fn = &trimmed[fn_pos + 3..];
700 let fn_name: String = after_fn
702 .chars()
703 .take_while(|c| c.is_alphanumeric() || *c == '_')
704 .collect();
705
706 if !fn_name.is_empty() {
707 return Some(format!("{}::{}", crate_name_normalized, fn_name));
708 }
709 }
710 if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
713 found_benchmark_attr = false;
714 }
715 }
716 }
717
718 None
719}
720
721pub fn resolve_default_function(
736 project_root: &Path,
737 crate_name: &str,
738 crate_dir: Option<&Path>,
739) -> String {
740 let crate_name_normalized = crate_name.replace('-', "_");
741
742 let search_dirs: Vec<PathBuf> = if let Some(dir) = crate_dir {
744 vec![dir.to_path_buf()]
745 } else {
746 vec![
747 project_root.join("bench-mobile"),
748 project_root.join("crates").join(crate_name),
749 project_root.to_path_buf(),
750 ]
751 };
752
753 for dir in &search_dirs {
755 if dir.join("Cargo.toml").exists() {
756 if let Some(detected) = detect_default_function(dir, &crate_name_normalized) {
757 return detected;
758 }
759 }
760 }
761
762 format!("{}::example_benchmark", crate_name_normalized)
764}
765
766pub fn ensure_android_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
777 ensure_android_project_with_options(output_dir, crate_name, None, None)
778}
779
780pub fn ensure_android_project_with_options(
792 output_dir: &Path,
793 crate_name: &str,
794 project_root: Option<&Path>,
795 crate_dir: Option<&Path>,
796) -> Result<(), BenchError> {
797 if android_project_exists(output_dir) {
798 return Ok(());
799 }
800
801 println!("Android project not found, generating scaffolding...");
802 let project_slug = crate_name.replace('-', "_");
803
804 let effective_root = project_root.unwrap_or_else(|| {
806 output_dir.parent().unwrap_or(output_dir)
807 });
808 let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
809
810 generate_android_project(output_dir, &project_slug, &default_function)?;
811 println!(" Generated Android project at {:?}", output_dir.join("android"));
812 println!(" Default benchmark function: {}", default_function);
813 Ok(())
814}
815
816pub fn ensure_ios_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
827 ensure_ios_project_with_options(output_dir, crate_name, None, None)
828}
829
830pub fn ensure_ios_project_with_options(
842 output_dir: &Path,
843 crate_name: &str,
844 project_root: Option<&Path>,
845 crate_dir: Option<&Path>,
846) -> Result<(), BenchError> {
847 if ios_project_exists(output_dir) {
848 return Ok(());
849 }
850
851 println!("iOS project not found, generating scaffolding...");
852 let project_pascal = "BenchRunner";
854 let library_name = crate_name.replace('-', "_");
856 let bundle_id_component = sanitize_bundle_id_component(crate_name);
859 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
860
861 let effective_root = project_root.unwrap_or_else(|| {
863 output_dir.parent().unwrap_or(output_dir)
864 });
865 let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
866
867 generate_ios_project(output_dir, &library_name, project_pascal, &bundle_prefix, &default_function)?;
868 println!(" Generated iOS project at {:?}", output_dir.join("ios"));
869 println!(" Default benchmark function: {}", default_function);
870 Ok(())
871}
872
873#[cfg(test)]
874mod tests {
875 use super::*;
876 use std::env;
877
878 #[test]
879 fn test_generate_bench_mobile_crate() {
880 let temp_dir = env::temp_dir().join("mobench-sdk-test");
881 fs::create_dir_all(&temp_dir).unwrap();
882
883 let result = generate_bench_mobile_crate(&temp_dir, "test_project");
884 assert!(result.is_ok());
885
886 assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
888 assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
889 assert!(temp_dir.join("bench-mobile/build.rs").exists());
890
891 fs::remove_dir_all(&temp_dir).ok();
893 }
894
895 #[test]
896 fn test_generate_android_project_no_unreplaced_placeholders() {
897 let temp_dir = env::temp_dir().join("mobench-sdk-android-test");
898 let _ = fs::remove_dir_all(&temp_dir);
900 fs::create_dir_all(&temp_dir).unwrap();
901
902 let result = generate_android_project(&temp_dir, "my-bench-project", "my_bench_project::test_func");
903 assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
904
905 let android_dir = temp_dir.join("android");
907 assert!(android_dir.join("settings.gradle").exists());
908 assert!(android_dir.join("app/build.gradle").exists());
909 assert!(android_dir.join("app/src/main/AndroidManifest.xml").exists());
910 assert!(android_dir.join("app/src/main/res/values/strings.xml").exists());
911 assert!(android_dir.join("app/src/main/res/values/themes.xml").exists());
912
913 let files_to_check = [
915 "settings.gradle",
916 "app/build.gradle",
917 "app/src/main/AndroidManifest.xml",
918 "app/src/main/res/values/strings.xml",
919 "app/src/main/res/values/themes.xml",
920 ];
921
922 for file in files_to_check {
923 let path = android_dir.join(file);
924 let contents = fs::read_to_string(&path).expect(&format!("Failed to read {}", file));
925
926 let has_placeholder = contents.contains("{{") && contents.contains("}}");
928 assert!(
929 !has_placeholder,
930 "File {} contains unreplaced template placeholders: {}",
931 file,
932 contents
933 );
934 }
935
936 let settings = fs::read_to_string(android_dir.join("settings.gradle")).unwrap();
938 assert!(
939 settings.contains("my-bench-project-android") || settings.contains("my_bench_project-android"),
940 "settings.gradle should contain project name"
941 );
942
943 let build_gradle = fs::read_to_string(android_dir.join("app/build.gradle")).unwrap();
944 assert!(
945 build_gradle.contains("dev.world.my-bench-project") || build_gradle.contains("dev.world.my_bench_project"),
946 "build.gradle should contain package name"
947 );
948
949 let manifest = fs::read_to_string(android_dir.join("app/src/main/AndroidManifest.xml")).unwrap();
950 assert!(
951 manifest.contains("Theme.MyBenchProject"),
952 "AndroidManifest.xml should contain PascalCase theme name"
953 );
954
955 let strings = fs::read_to_string(android_dir.join("app/src/main/res/values/strings.xml")).unwrap();
956 assert!(
957 strings.contains("Benchmark"),
958 "strings.xml should contain app name with Benchmark"
959 );
960
961 fs::remove_dir_all(&temp_dir).ok();
963 }
964
965 #[test]
966 fn test_is_template_file() {
967 assert!(is_template_file(Path::new("settings.gradle")));
968 assert!(is_template_file(Path::new("app/build.gradle")));
969 assert!(is_template_file(Path::new("AndroidManifest.xml")));
970 assert!(is_template_file(Path::new("strings.xml")));
971 assert!(is_template_file(Path::new("MainActivity.kt.template")));
972 assert!(is_template_file(Path::new("project.yml")));
973 assert!(is_template_file(Path::new("Info.plist")));
974 assert!(!is_template_file(Path::new("libfoo.so")));
975 assert!(!is_template_file(Path::new("image.png")));
976 }
977
978 #[test]
979 fn test_validate_no_unreplaced_placeholders() {
980 assert!(validate_no_unreplaced_placeholders("hello world", Path::new("test.txt")).is_ok());
982
983 assert!(validate_no_unreplaced_placeholders("${ENV_VAR}", Path::new("test.txt")).is_ok());
985
986 let result = validate_no_unreplaced_placeholders("hello {{NAME}}", Path::new("test.txt"));
988 assert!(result.is_err());
989 let err = result.unwrap_err().to_string();
990 assert!(err.contains("{{NAME}}"));
991 }
992
993 #[test]
994 fn test_to_pascal_case() {
995 assert_eq!(to_pascal_case("my-project"), "MyProject");
996 assert_eq!(to_pascal_case("my_project"), "MyProject");
997 assert_eq!(to_pascal_case("myproject"), "Myproject");
998 assert_eq!(to_pascal_case("my-bench-project"), "MyBenchProject");
999 }
1000
1001 #[test]
1002 fn test_detect_default_function_finds_benchmark() {
1003 let temp_dir = env::temp_dir().join("mobench-sdk-detect-test");
1004 let _ = fs::remove_dir_all(&temp_dir);
1005 fs::create_dir_all(temp_dir.join("src")).unwrap();
1006
1007 let lib_content = r#"
1009use mobench_sdk::benchmark;
1010
1011/// Some docs
1012#[benchmark]
1013fn my_benchmark_func() {
1014 // benchmark code
1015}
1016
1017fn helper_func() {}
1018"#;
1019 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1020 fs::write(temp_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
1021
1022 let result = detect_default_function(&temp_dir, "my_crate");
1023 assert_eq!(result, Some("my_crate::my_benchmark_func".to_string()));
1024
1025 fs::remove_dir_all(&temp_dir).ok();
1027 }
1028
1029 #[test]
1030 fn test_detect_default_function_no_benchmark() {
1031 let temp_dir = env::temp_dir().join("mobench-sdk-detect-none-test");
1032 let _ = fs::remove_dir_all(&temp_dir);
1033 fs::create_dir_all(temp_dir.join("src")).unwrap();
1034
1035 let lib_content = r#"
1037fn regular_function() {
1038 // no benchmark here
1039}
1040"#;
1041 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1042
1043 let result = detect_default_function(&temp_dir, "my_crate");
1044 assert!(result.is_none());
1045
1046 fs::remove_dir_all(&temp_dir).ok();
1048 }
1049
1050 #[test]
1051 fn test_detect_default_function_pub_fn() {
1052 let temp_dir = env::temp_dir().join("mobench-sdk-detect-pub-test");
1053 let _ = fs::remove_dir_all(&temp_dir);
1054 fs::create_dir_all(temp_dir.join("src")).unwrap();
1055
1056 let lib_content = r#"
1058#[benchmark]
1059pub fn public_bench() {
1060 // benchmark code
1061}
1062"#;
1063 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1064
1065 let result = detect_default_function(&temp_dir, "test-crate");
1066 assert_eq!(result, Some("test_crate::public_bench".to_string()));
1067
1068 fs::remove_dir_all(&temp_dir).ok();
1070 }
1071
1072 #[test]
1073 fn test_resolve_default_function_fallback() {
1074 let temp_dir = env::temp_dir().join("mobench-sdk-resolve-test");
1075 let _ = fs::remove_dir_all(&temp_dir);
1076 fs::create_dir_all(&temp_dir).unwrap();
1077
1078 let result = resolve_default_function(&temp_dir, "my-crate", None);
1080 assert_eq!(result, "my_crate::example_benchmark");
1081
1082 fs::remove_dir_all(&temp_dir).ok();
1084 }
1085
1086 #[test]
1087 fn test_sanitize_bundle_id_component() {
1088 assert_eq!(sanitize_bundle_id_component("bench-mobile"), "benchmobile");
1090 assert_eq!(sanitize_bundle_id_component("bench_mobile"), "benchmobile");
1092 assert_eq!(sanitize_bundle_id_component("my-project_name"), "myprojectname");
1094 assert_eq!(sanitize_bundle_id_component("benchmobile"), "benchmobile");
1096 assert_eq!(sanitize_bundle_id_component("bench2mobile"), "bench2mobile");
1098 assert_eq!(sanitize_bundle_id_component("BenchMobile"), "benchmobile");
1100 assert_eq!(sanitize_bundle_id_component("My-Complex_Project-123"), "mycomplexproject123");
1102 }
1103}