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 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_package_dir(&target_dir, &package_name)?;
333
334 Ok(())
335}
336
337fn move_kotlin_files_to_package_dir(android_dir: &Path, package_name: &str) -> Result<(), BenchError> {
347 let package_path = package_name.replace('.', "/");
349
350 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 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
363fn 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 return Ok(());
369 }
370
371 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 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
399pub 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 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 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
457fn 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
491fn 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
543const 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 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 let mut relative = file.path().to_path_buf();
569 let mut contents = file.contents().to_vec();
570
571 let is_explicit_template = relative
573 .extension()
574 .map(|ext| ext == "template")
575 .unwrap_or(false);
576
577 let should_render = is_explicit_template || is_template_file(&relative);
579
580 if is_explicit_template {
581 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_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
605fn is_template_file(path: &Path) -> bool {
608 if let Some(ext) = path.extension() {
610 if ext == "template" {
611 return true;
612 }
613 if let Some(ext_str) = ext.to_str() {
615 return TEMPLATE_EXTENSIONS.contains(&ext_str);
616 }
617 }
618 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
630fn validate_no_unreplaced_placeholders(content: &str, file_path: &Path) -> Result<(), BenchError> {
632 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 let var_name = &content[abs_start + 2..abs_start + end];
642 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
673pub 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
704pub 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
718pub 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
726pub fn ios_project_exists(output_dir: &Path) -> bool {
730 output_dir.join("ios/BenchRunner/project.yml").exists()
731}
732
733pub 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 if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
764 found_benchmark_attr = true;
765 continue;
766 }
767
768 if found_benchmark_attr {
770 if let Some(fn_pos) = trimmed.find("fn ") {
772 let after_fn = &trimmed[fn_pos + 3..];
773 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 if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
786 found_benchmark_attr = false;
787 }
788 }
789 }
790
791 None
792}
793
794pub fn resolve_default_function(
809 project_root: &Path,
810 crate_name: &str,
811 crate_dir: Option<&Path>,
812) -> String {
813 let crate_name_normalized = crate_name.replace('-', "_");
814
815 let search_dirs: Vec<PathBuf> = if let Some(dir) = crate_dir {
817 vec![dir.to_path_buf()]
818 } else {
819 vec![
820 project_root.join("bench-mobile"),
821 project_root.join("crates").join(crate_name),
822 project_root.to_path_buf(),
823 ]
824 };
825
826 for dir in &search_dirs {
828 if dir.join("Cargo.toml").exists() {
829 if let Some(detected) = detect_default_function(dir, &crate_name_normalized) {
830 return detected;
831 }
832 }
833 }
834
835 format!("{}::example_benchmark", crate_name_normalized)
837}
838
839pub fn ensure_android_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
850 ensure_android_project_with_options(output_dir, crate_name, None, None)
851}
852
853pub fn ensure_android_project_with_options(
865 output_dir: &Path,
866 crate_name: &str,
867 project_root: Option<&Path>,
868 crate_dir: Option<&Path>,
869) -> Result<(), BenchError> {
870 if android_project_exists(output_dir) {
871 return Ok(());
872 }
873
874 println!("Android project not found, generating scaffolding...");
875 let project_slug = crate_name.replace('-', "_");
876
877 let effective_root = project_root.unwrap_or_else(|| {
879 output_dir.parent().unwrap_or(output_dir)
880 });
881 let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
882
883 generate_android_project(output_dir, &project_slug, &default_function)?;
884 println!(" Generated Android project at {:?}", output_dir.join("android"));
885 println!(" Default benchmark function: {}", default_function);
886 Ok(())
887}
888
889pub fn ensure_ios_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
900 ensure_ios_project_with_options(output_dir, crate_name, None, None)
901}
902
903pub fn ensure_ios_project_with_options(
915 output_dir: &Path,
916 crate_name: &str,
917 project_root: Option<&Path>,
918 crate_dir: Option<&Path>,
919) -> Result<(), BenchError> {
920 if ios_project_exists(output_dir) {
921 return Ok(());
922 }
923
924 println!("iOS project not found, generating scaffolding...");
925 let project_pascal = "BenchRunner";
927 let library_name = crate_name.replace('-', "_");
929 let bundle_id_component = sanitize_bundle_id_component(crate_name);
932 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
933
934 let effective_root = project_root.unwrap_or_else(|| {
936 output_dir.parent().unwrap_or(output_dir)
937 });
938 let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
939
940 generate_ios_project(output_dir, &library_name, project_pascal, &bundle_prefix, &default_function)?;
941 println!(" Generated iOS project at {:?}", output_dir.join("ios"));
942 println!(" Default benchmark function: {}", default_function);
943 Ok(())
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use std::env;
950
951 #[test]
952 fn test_generate_bench_mobile_crate() {
953 let temp_dir = env::temp_dir().join("mobench-sdk-test");
954 fs::create_dir_all(&temp_dir).unwrap();
955
956 let result = generate_bench_mobile_crate(&temp_dir, "test_project");
957 assert!(result.is_ok());
958
959 assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
961 assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
962 assert!(temp_dir.join("bench-mobile/build.rs").exists());
963
964 fs::remove_dir_all(&temp_dir).ok();
966 }
967
968 #[test]
969 fn test_generate_android_project_no_unreplaced_placeholders() {
970 let temp_dir = env::temp_dir().join("mobench-sdk-android-test");
971 let _ = fs::remove_dir_all(&temp_dir);
973 fs::create_dir_all(&temp_dir).unwrap();
974
975 let result = generate_android_project(&temp_dir, "my-bench-project", "my_bench_project::test_func");
976 assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
977
978 let android_dir = temp_dir.join("android");
980 assert!(android_dir.join("settings.gradle").exists());
981 assert!(android_dir.join("app/build.gradle").exists());
982 assert!(android_dir.join("app/src/main/AndroidManifest.xml").exists());
983 assert!(android_dir.join("app/src/main/res/values/strings.xml").exists());
984 assert!(android_dir.join("app/src/main/res/values/themes.xml").exists());
985
986 let files_to_check = [
988 "settings.gradle",
989 "app/build.gradle",
990 "app/src/main/AndroidManifest.xml",
991 "app/src/main/res/values/strings.xml",
992 "app/src/main/res/values/themes.xml",
993 ];
994
995 for file in files_to_check {
996 let path = android_dir.join(file);
997 let contents = fs::read_to_string(&path).expect(&format!("Failed to read {}", file));
998
999 let has_placeholder = contents.contains("{{") && contents.contains("}}");
1001 assert!(
1002 !has_placeholder,
1003 "File {} contains unreplaced template placeholders: {}",
1004 file,
1005 contents
1006 );
1007 }
1008
1009 let settings = fs::read_to_string(android_dir.join("settings.gradle")).unwrap();
1011 assert!(
1012 settings.contains("my-bench-project-android") || settings.contains("my_bench_project-android"),
1013 "settings.gradle should contain project name"
1014 );
1015
1016 let build_gradle = fs::read_to_string(android_dir.join("app/build.gradle")).unwrap();
1017 assert!(
1019 build_gradle.contains("dev.world.mybenchproject"),
1020 "build.gradle should contain sanitized package name 'dev.world.mybenchproject'"
1021 );
1022
1023 let manifest = fs::read_to_string(android_dir.join("app/src/main/AndroidManifest.xml")).unwrap();
1024 assert!(
1025 manifest.contains("Theme.MyBenchProject"),
1026 "AndroidManifest.xml should contain PascalCase theme name"
1027 );
1028
1029 let strings = fs::read_to_string(android_dir.join("app/src/main/res/values/strings.xml")).unwrap();
1030 assert!(
1031 strings.contains("Benchmark"),
1032 "strings.xml should contain app name with Benchmark"
1033 );
1034
1035 let main_activity_path = android_dir.join("app/src/main/java/dev/world/mybenchproject/MainActivity.kt");
1038 assert!(
1039 main_activity_path.exists(),
1040 "MainActivity.kt should be in package directory: {:?}",
1041 main_activity_path
1042 );
1043
1044 let test_activity_path = android_dir.join("app/src/androidTest/java/dev/world/mybenchproject/MainActivityTest.kt");
1045 assert!(
1046 test_activity_path.exists(),
1047 "MainActivityTest.kt should be in package directory: {:?}",
1048 test_activity_path
1049 );
1050
1051 assert!(
1053 !android_dir.join("app/src/main/java/MainActivity.kt").exists(),
1054 "MainActivity.kt should not be in root java directory"
1055 );
1056 assert!(
1057 !android_dir.join("app/src/androidTest/java/MainActivityTest.kt").exists(),
1058 "MainActivityTest.kt should not be in root java directory"
1059 );
1060
1061 fs::remove_dir_all(&temp_dir).ok();
1063 }
1064
1065 #[test]
1066 fn test_is_template_file() {
1067 assert!(is_template_file(Path::new("settings.gradle")));
1068 assert!(is_template_file(Path::new("app/build.gradle")));
1069 assert!(is_template_file(Path::new("AndroidManifest.xml")));
1070 assert!(is_template_file(Path::new("strings.xml")));
1071 assert!(is_template_file(Path::new("MainActivity.kt.template")));
1072 assert!(is_template_file(Path::new("project.yml")));
1073 assert!(is_template_file(Path::new("Info.plist")));
1074 assert!(!is_template_file(Path::new("libfoo.so")));
1075 assert!(!is_template_file(Path::new("image.png")));
1076 }
1077
1078 #[test]
1079 fn test_validate_no_unreplaced_placeholders() {
1080 assert!(validate_no_unreplaced_placeholders("hello world", Path::new("test.txt")).is_ok());
1082
1083 assert!(validate_no_unreplaced_placeholders("${ENV_VAR}", Path::new("test.txt")).is_ok());
1085
1086 let result = validate_no_unreplaced_placeholders("hello {{NAME}}", Path::new("test.txt"));
1088 assert!(result.is_err());
1089 let err = result.unwrap_err().to_string();
1090 assert!(err.contains("{{NAME}}"));
1091 }
1092
1093 #[test]
1094 fn test_to_pascal_case() {
1095 assert_eq!(to_pascal_case("my-project"), "MyProject");
1096 assert_eq!(to_pascal_case("my_project"), "MyProject");
1097 assert_eq!(to_pascal_case("myproject"), "Myproject");
1098 assert_eq!(to_pascal_case("my-bench-project"), "MyBenchProject");
1099 }
1100
1101 #[test]
1102 fn test_detect_default_function_finds_benchmark() {
1103 let temp_dir = env::temp_dir().join("mobench-sdk-detect-test");
1104 let _ = fs::remove_dir_all(&temp_dir);
1105 fs::create_dir_all(temp_dir.join("src")).unwrap();
1106
1107 let lib_content = r#"
1109use mobench_sdk::benchmark;
1110
1111/// Some docs
1112#[benchmark]
1113fn my_benchmark_func() {
1114 // benchmark code
1115}
1116
1117fn helper_func() {}
1118"#;
1119 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1120 fs::write(temp_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
1121
1122 let result = detect_default_function(&temp_dir, "my_crate");
1123 assert_eq!(result, Some("my_crate::my_benchmark_func".to_string()));
1124
1125 fs::remove_dir_all(&temp_dir).ok();
1127 }
1128
1129 #[test]
1130 fn test_detect_default_function_no_benchmark() {
1131 let temp_dir = env::temp_dir().join("mobench-sdk-detect-none-test");
1132 let _ = fs::remove_dir_all(&temp_dir);
1133 fs::create_dir_all(temp_dir.join("src")).unwrap();
1134
1135 let lib_content = r#"
1137fn regular_function() {
1138 // no benchmark here
1139}
1140"#;
1141 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1142
1143 let result = detect_default_function(&temp_dir, "my_crate");
1144 assert!(result.is_none());
1145
1146 fs::remove_dir_all(&temp_dir).ok();
1148 }
1149
1150 #[test]
1151 fn test_detect_default_function_pub_fn() {
1152 let temp_dir = env::temp_dir().join("mobench-sdk-detect-pub-test");
1153 let _ = fs::remove_dir_all(&temp_dir);
1154 fs::create_dir_all(temp_dir.join("src")).unwrap();
1155
1156 let lib_content = r#"
1158#[benchmark]
1159pub fn public_bench() {
1160 // benchmark code
1161}
1162"#;
1163 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1164
1165 let result = detect_default_function(&temp_dir, "test-crate");
1166 assert_eq!(result, Some("test_crate::public_bench".to_string()));
1167
1168 fs::remove_dir_all(&temp_dir).ok();
1170 }
1171
1172 #[test]
1173 fn test_resolve_default_function_fallback() {
1174 let temp_dir = env::temp_dir().join("mobench-sdk-resolve-test");
1175 let _ = fs::remove_dir_all(&temp_dir);
1176 fs::create_dir_all(&temp_dir).unwrap();
1177
1178 let result = resolve_default_function(&temp_dir, "my-crate", None);
1180 assert_eq!(result, "my_crate::example_benchmark");
1181
1182 fs::remove_dir_all(&temp_dir).ok();
1184 }
1185
1186 #[test]
1187 fn test_sanitize_bundle_id_component() {
1188 assert_eq!(sanitize_bundle_id_component("bench-mobile"), "benchmobile");
1190 assert_eq!(sanitize_bundle_id_component("bench_mobile"), "benchmobile");
1192 assert_eq!(sanitize_bundle_id_component("my-project_name"), "myprojectname");
1194 assert_eq!(sanitize_bundle_id_component("benchmobile"), "benchmobile");
1196 assert_eq!(sanitize_bundle_id_component("bench2mobile"), "bench2mobile");
1198 assert_eq!(sanitize_bundle_id_component("BenchMobile"), "benchmobile");
1200 assert_eq!(sanitize_bundle_id_component("My-Complex_Project-123"), "mycomplexproject123");
1202 }
1203
1204 #[test]
1205 fn test_generate_ios_project_bundle_id_not_duplicated() {
1206 let temp_dir = env::temp_dir().join("mobench-sdk-ios-bundle-test");
1207 let _ = fs::remove_dir_all(&temp_dir);
1209 fs::create_dir_all(&temp_dir).unwrap();
1210
1211 let crate_name = "bench-mobile";
1213 let bundle_prefix = "dev.world.benchmobile";
1214 let project_pascal = "BenchRunner";
1215
1216 let result = generate_ios_project(
1217 &temp_dir,
1218 crate_name,
1219 project_pascal,
1220 bundle_prefix,
1221 "bench_mobile::test_func",
1222 );
1223 assert!(result.is_ok(), "generate_ios_project failed: {:?}", result.err());
1224
1225 let project_yml_path = temp_dir.join("ios/BenchRunner/project.yml");
1227 assert!(project_yml_path.exists(), "project.yml should exist");
1228
1229 let project_yml = fs::read_to_string(&project_yml_path).unwrap();
1231
1232 assert!(
1235 project_yml.contains("dev.world.benchmobile.BenchRunner"),
1236 "Bundle ID should be 'dev.world.benchmobile.BenchRunner', got:\n{}",
1237 project_yml
1238 );
1239 assert!(
1240 !project_yml.contains("dev.world.benchmobile.benchmobile"),
1241 "Bundle ID should NOT be duplicated as 'dev.world.benchmobile.benchmobile', got:\n{}",
1242 project_yml
1243 );
1244
1245 fs::remove_dir_all(&temp_dir).ok();
1247 }
1248
1249 #[test]
1250 fn test_cross_platform_naming_consistency() {
1251 let temp_dir = env::temp_dir().join("mobench-sdk-naming-consistency-test");
1253 let _ = fs::remove_dir_all(&temp_dir);
1254 fs::create_dir_all(&temp_dir).unwrap();
1255
1256 let project_name = "bench-mobile";
1257
1258 let result = generate_android_project(&temp_dir, project_name, "bench_mobile::test_func");
1260 assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
1261
1262 let bundle_id_component = sanitize_bundle_id_component(project_name);
1264 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1265 let result = generate_ios_project(
1266 &temp_dir,
1267 &project_name.replace('-', "_"),
1268 "BenchRunner",
1269 &bundle_prefix,
1270 "bench_mobile::test_func",
1271 );
1272 assert!(result.is_ok(), "generate_ios_project failed: {:?}", result.err());
1273
1274 let android_build_gradle = fs::read_to_string(
1276 temp_dir.join("android/app/build.gradle")
1277 ).expect("Failed to read Android build.gradle");
1278
1279 let ios_project_yml = fs::read_to_string(
1281 temp_dir.join("ios/BenchRunner/project.yml")
1282 ).expect("Failed to read iOS project.yml");
1283
1284 assert!(
1288 android_build_gradle.contains("dev.world.benchmobile"),
1289 "Android package should be 'dev.world.benchmobile', got:\n{}",
1290 android_build_gradle
1291 );
1292 assert!(
1293 ios_project_yml.contains("dev.world.benchmobile"),
1294 "iOS bundle prefix should contain 'dev.world.benchmobile', got:\n{}",
1295 ios_project_yml
1296 );
1297
1298 assert!(
1300 !android_build_gradle.contains("dev.world.bench-mobile"),
1301 "Android package should NOT contain hyphens"
1302 );
1303 assert!(
1304 !android_build_gradle.contains("dev.world.bench_mobile"),
1305 "Android package should NOT contain underscores"
1306 );
1307
1308 fs::remove_dir_all(&temp_dir).ok();
1310 }
1311
1312 #[test]
1313 fn test_cross_platform_version_consistency() {
1314 let temp_dir = env::temp_dir().join("mobench-sdk-version-consistency-test");
1316 let _ = fs::remove_dir_all(&temp_dir);
1317 fs::create_dir_all(&temp_dir).unwrap();
1318
1319 let project_name = "test-project";
1320
1321 let result = generate_android_project(&temp_dir, project_name, "test_project::test_func");
1323 assert!(result.is_ok(), "generate_android_project failed: {:?}", result.err());
1324
1325 let bundle_id_component = sanitize_bundle_id_component(project_name);
1327 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1328 let result = generate_ios_project(
1329 &temp_dir,
1330 &project_name.replace('-', "_"),
1331 "BenchRunner",
1332 &bundle_prefix,
1333 "test_project::test_func",
1334 );
1335 assert!(result.is_ok(), "generate_ios_project failed: {:?}", result.err());
1336
1337 let android_build_gradle = fs::read_to_string(
1339 temp_dir.join("android/app/build.gradle")
1340 ).expect("Failed to read Android build.gradle");
1341
1342 let ios_project_yml = fs::read_to_string(
1344 temp_dir.join("ios/BenchRunner/project.yml")
1345 ).expect("Failed to read iOS project.yml");
1346
1347 assert!(
1349 android_build_gradle.contains("versionName \"1.0.0\""),
1350 "Android versionName should be '1.0.0', got:\n{}",
1351 android_build_gradle
1352 );
1353 assert!(
1354 ios_project_yml.contains("CFBundleShortVersionString: \"1.0.0\""),
1355 "iOS CFBundleShortVersionString should be '1.0.0', got:\n{}",
1356 ios_project_yml
1357 );
1358
1359 fs::remove_dir_all(&temp_dir).ok();
1361 }
1362
1363 #[test]
1364 fn test_bundle_id_prefix_consistency() {
1365 let test_cases = vec![
1367 ("my-project", "dev.world.myproject"),
1368 ("bench_mobile", "dev.world.benchmobile"),
1369 ("TestApp", "dev.world.testapp"),
1370 ("app-with-many-dashes", "dev.world.appwithmanydashes"),
1371 ("app_with_many_underscores", "dev.world.appwithmanyunderscores"),
1372 ];
1373
1374 for (input, expected_prefix) in test_cases {
1375 let sanitized = sanitize_bundle_id_component(input);
1376 let full_prefix = format!("dev.world.{}", sanitized);
1377 assert_eq!(
1378 full_prefix, expected_prefix,
1379 "For input '{}', expected '{}' but got '{}'",
1380 input, expected_prefix, full_prefix
1381 );
1382 }
1383 }
1384}