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(
64 output_dir,
65 &project_slug,
66 &project_pascal,
67 &bundle_prefix,
68 default_function,
69 )?;
70 }
71 Target::Both => {
72 generate_android_project(output_dir, &project_slug, default_function)?;
73 generate_ios_project(
74 output_dir,
75 &project_slug,
76 &project_pascal,
77 &bundle_prefix,
78 default_function,
79 )?;
80 }
81 }
82
83 generate_config_file(output_dir, config)?;
85
86 if config.generate_examples {
88 generate_example_benchmarks(output_dir)?;
89 }
90
91 Ok(output_dir.clone())
92}
93
94fn generate_bench_mobile_crate(output_dir: &Path, project_name: &str) -> Result<(), BenchError> {
96 let crate_dir = output_dir.join("bench-mobile");
97 fs::create_dir_all(crate_dir.join("src"))?;
98
99 let crate_name = format!("{}-bench-mobile", project_name);
100
101 let cargo_toml = format!(
105 r#"[package]
106name = "{}"
107version = "0.1.0"
108edition = "2021"
109
110[lib]
111crate-type = ["cdylib", "staticlib", "rlib"]
112
113[dependencies]
114mobench-sdk = {{ path = ".." }}
115uniffi = "0.28"
116{} = {{ path = ".." }}
117
118[features]
119default = []
120
121[build-dependencies]
122uniffi = {{ version = "0.28", features = ["build"] }}
123
124# Binary for generating UniFFI bindings (used by mobench build)
125[[bin]]
126name = "uniffi-bindgen"
127path = "src/bin/uniffi-bindgen.rs"
128
129# IMPORTANT: If your project uses rustls (directly or transitively), you must configure
130# it to use the 'ring' crypto backend instead of 'aws-lc-rs' (the default in rustls 0.23+).
131# aws-lc-rs doesn't compile for Android NDK targets due to C compilation issues.
132#
133# Add this to your root Cargo.toml:
134# [workspace.dependencies]
135# rustls = {{ version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }}
136#
137# Then in each crate that uses rustls:
138# [dependencies]
139# rustls = {{ workspace = true }}
140"#,
141 crate_name, project_name
142 );
143
144 fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
145
146 let lib_rs_template = r#"//! Mobile FFI bindings for benchmarks
148//!
149//! This crate provides the FFI boundary between Rust benchmarks and mobile
150//! platforms (Android/iOS). It uses UniFFI to generate type-safe bindings.
151
152use uniffi;
153
154// Ensure the user crate is linked so benchmark registrations are pulled in.
155extern crate {{USER_CRATE}} as _bench_user_crate;
156
157// Re-export mobench-sdk types with UniFFI annotations
158#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
159pub struct BenchSpec {
160 pub name: String,
161 pub iterations: u32,
162 pub warmup: u32,
163}
164
165#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
166pub struct BenchSample {
167 pub duration_ns: u64,
168}
169
170#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
171pub struct SemanticPhase {
172 pub name: String,
173 pub duration_ns: u64,
174}
175
176#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
177pub struct HarnessTimelineSpan {
178 pub phase: String,
179 pub start_offset_ns: u64,
180 pub end_offset_ns: u64,
181 pub iteration: Option<u32>,
182}
183
184#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
185pub struct BenchReport {
186 pub spec: BenchSpec,
187 pub samples: Vec<BenchSample>,
188 pub phases: Vec<SemanticPhase>,
189 pub timeline: Vec<HarnessTimelineSpan>,
190}
191
192#[derive(Debug, thiserror::Error, uniffi::Error)]
193#[uniffi(flat_error)]
194pub enum BenchError {
195 #[error("iterations must be greater than zero")]
196 InvalidIterations,
197
198 #[error("unknown benchmark function: {name}")]
199 UnknownFunction { name: String },
200
201 #[error("benchmark execution failed: {reason}")]
202 ExecutionFailed { reason: String },
203}
204
205// Convert from mobench-sdk types
206impl From<mobench_sdk::BenchSpec> for BenchSpec {
207 fn from(spec: mobench_sdk::BenchSpec) -> Self {
208 Self {
209 name: spec.name,
210 iterations: spec.iterations,
211 warmup: spec.warmup,
212 }
213 }
214}
215
216impl From<BenchSpec> for mobench_sdk::BenchSpec {
217 fn from(spec: BenchSpec) -> Self {
218 Self {
219 name: spec.name,
220 iterations: spec.iterations,
221 warmup: spec.warmup,
222 }
223 }
224}
225
226impl From<mobench_sdk::BenchSample> for BenchSample {
227 fn from(sample: mobench_sdk::BenchSample) -> Self {
228 Self {
229 duration_ns: sample.duration_ns,
230 }
231 }
232}
233
234impl From<mobench_sdk::SemanticPhase> for SemanticPhase {
235 fn from(phase: mobench_sdk::SemanticPhase) -> Self {
236 Self {
237 name: phase.name,
238 duration_ns: phase.duration_ns,
239 }
240 }
241}
242
243impl From<mobench_sdk::HarnessTimelineSpan> for HarnessTimelineSpan {
244 fn from(span: mobench_sdk::HarnessTimelineSpan) -> Self {
245 Self {
246 phase: span.phase,
247 start_offset_ns: span.start_offset_ns,
248 end_offset_ns: span.end_offset_ns,
249 iteration: span.iteration,
250 }
251 }
252}
253
254impl From<mobench_sdk::RunnerReport> for BenchReport {
255 fn from(report: mobench_sdk::RunnerReport) -> Self {
256 Self {
257 spec: report.spec.into(),
258 samples: report.samples.into_iter().map(Into::into).collect(),
259 phases: report.phases.into_iter().map(Into::into).collect(),
260 timeline: report.timeline.into_iter().map(Into::into).collect(),
261 }
262 }
263}
264
265impl From<mobench_sdk::BenchError> for BenchError {
266 fn from(err: mobench_sdk::BenchError) -> Self {
267 match err {
268 mobench_sdk::BenchError::Runner(runner_err) => {
269 BenchError::ExecutionFailed {
270 reason: runner_err.to_string(),
271 }
272 }
273 mobench_sdk::BenchError::UnknownFunction(name, _available) => {
274 BenchError::UnknownFunction { name }
275 }
276 _ => BenchError::ExecutionFailed {
277 reason: err.to_string(),
278 },
279 }
280 }
281}
282
283/// Runs a benchmark by name with the given specification
284///
285/// This is the main FFI entry point called from mobile platforms.
286#[uniffi::export]
287pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
288 let sdk_spec: mobench_sdk::BenchSpec = spec.into();
289 let report = mobench_sdk::run_benchmark(sdk_spec)?;
290 Ok(report.into())
291}
292
293// Generate UniFFI scaffolding
294uniffi::setup_scaffolding!();
295"#;
296
297 let lib_rs = render_template(
298 lib_rs_template,
299 &[TemplateVar {
300 name: "USER_CRATE",
301 value: project_name.replace('-', "_"),
302 }],
303 );
304 fs::write(crate_dir.join("src/lib.rs"), lib_rs)?;
305
306 let build_rs = r#"fn main() {
308 uniffi::generate_scaffolding("src/lib.rs").unwrap();
309}
310"#;
311
312 fs::write(crate_dir.join("build.rs"), build_rs)?;
313
314 let bin_dir = crate_dir.join("src/bin");
316 fs::create_dir_all(&bin_dir)?;
317 let uniffi_bindgen_rs = r#"fn main() {
318 uniffi::uniffi_bindgen_main()
319}
320"#;
321 fs::write(bin_dir.join("uniffi-bindgen.rs"), uniffi_bindgen_rs)?;
322
323 Ok(())
324}
325
326pub fn generate_android_project(
337 output_dir: &Path,
338 project_slug: &str,
339 default_function: &str,
340) -> Result<(), BenchError> {
341 let target_dir = output_dir.join("android");
342 let preserved_assets = collect_preserved_android_assets(&target_dir)?;
343 reset_generated_project_dir(&target_dir)?;
344 let library_name = project_slug.replace('-', "_");
345 let project_pascal = to_pascal_case(project_slug);
346 let package_id_component = sanitize_bundle_id_component(project_slug);
349 let package_name = format!("dev.world.{}", package_id_component);
350 let vars = vec![
351 TemplateVar {
352 name: "PROJECT_NAME",
353 value: project_slug.to_string(),
354 },
355 TemplateVar {
356 name: "PROJECT_NAME_PASCAL",
357 value: project_pascal.clone(),
358 },
359 TemplateVar {
360 name: "APP_NAME",
361 value: format!("{} Benchmark", project_pascal),
362 },
363 TemplateVar {
364 name: "PACKAGE_NAME",
365 value: package_name.clone(),
366 },
367 TemplateVar {
368 name: "UNIFFI_NAMESPACE",
369 value: library_name.clone(),
370 },
371 TemplateVar {
372 name: "LIBRARY_NAME",
373 value: library_name,
374 },
375 TemplateVar {
376 name: "DEFAULT_FUNCTION",
377 value: default_function.to_string(),
378 },
379 ];
380 render_dir(&ANDROID_TEMPLATES, &target_dir, &vars)?;
381 restore_preserved_android_assets(&target_dir, &preserved_assets)?;
382
383 move_kotlin_files_to_package_dir(&target_dir, &package_name)?;
386
387 Ok(())
388}
389
390fn collect_preserved_android_assets(
391 target_dir: &Path,
392) -> Result<Vec<(PathBuf, Vec<u8>)>, BenchError> {
393 let assets_dir = target_dir.join("app/src/main/assets");
394 let mut preserved = Vec::new();
395
396 if assets_dir.exists() {
397 collect_preserved_files(&assets_dir, &assets_dir, &mut preserved)?;
398 }
399
400 Ok(preserved)
401}
402
403fn collect_preserved_files(
404 root: &Path,
405 current: &Path,
406 preserved: &mut Vec<(PathBuf, Vec<u8>)>,
407) -> Result<(), BenchError> {
408 let mut entries = fs::read_dir(current)?
409 .collect::<Result<Vec<_>, _>>()
410 .map_err(BenchError::Io)?;
411 entries.sort_by_key(|entry| entry.path());
412
413 for entry in entries {
414 let path = entry.path();
415 if path.is_dir() {
416 collect_preserved_files(root, &path, preserved)?;
417 continue;
418 }
419
420 let relative = path.strip_prefix(root).map_err(|e| {
421 BenchError::Build(format!(
422 "Failed to preserve Android asset {:?}: {}",
423 path, e
424 ))
425 })?;
426 preserved.push((relative.to_path_buf(), fs::read(&path)?));
427 }
428
429 Ok(())
430}
431
432fn restore_preserved_android_assets(
433 target_dir: &Path,
434 preserved_assets: &[(PathBuf, Vec<u8>)],
435) -> Result<(), BenchError> {
436 if preserved_assets.is_empty() {
437 return Ok(());
438 }
439
440 let assets_dir = target_dir.join("app/src/main/assets");
441 for (relative, contents) in preserved_assets {
442 let asset_path = assets_dir.join(relative);
443 if let Some(parent) = asset_path.parent() {
444 fs::create_dir_all(parent)?;
445 }
446 fs::write(asset_path, contents)?;
447 }
448
449 Ok(())
450}
451
452fn reset_generated_project_dir(target_dir: &Path) -> Result<(), BenchError> {
453 if target_dir.exists() {
454 fs::remove_dir_all(target_dir).map_err(|e| {
455 BenchError::Build(format!(
456 "Failed to clear existing generated project at {:?}: {}",
457 target_dir, e
458 ))
459 })?;
460 }
461 Ok(())
462}
463
464fn move_kotlin_files_to_package_dir(
474 android_dir: &Path,
475 package_name: &str,
476) -> Result<(), BenchError> {
477 let package_path = package_name.replace('.', "/");
479
480 let main_java_dir = android_dir.join("app/src/main/java");
482 let main_package_dir = main_java_dir.join(&package_path);
483 move_kotlin_file(&main_java_dir, &main_package_dir, "MainActivity.kt")?;
484
485 let test_java_dir = android_dir.join("app/src/androidTest/java");
487 let test_package_dir = test_java_dir.join(&package_path);
488 move_kotlin_file(&test_java_dir, &test_package_dir, "MainActivityTest.kt")?;
489
490 Ok(())
491}
492
493fn move_kotlin_file(src_dir: &Path, dest_dir: &Path, filename: &str) -> Result<(), BenchError> {
495 let src_file = src_dir.join(filename);
496 if !src_file.exists() {
497 return Ok(());
499 }
500
501 fs::create_dir_all(dest_dir).map_err(|e| {
503 BenchError::Build(format!(
504 "Failed to create package directory {:?}: {}",
505 dest_dir, e
506 ))
507 })?;
508
509 let dest_file = dest_dir.join(filename);
510
511 fs::copy(&src_file, &dest_file).map_err(|e| {
513 BenchError::Build(format!(
514 "Failed to copy {} to {:?}: {}",
515 filename, dest_file, e
516 ))
517 })?;
518
519 fs::remove_file(&src_file).map_err(|e| {
520 BenchError::Build(format!(
521 "Failed to remove original file {:?}: {}",
522 src_file, e
523 ))
524 })?;
525
526 Ok(())
527}
528
529pub fn generate_ios_project(
542 output_dir: &Path,
543 project_slug: &str,
544 project_pascal: &str,
545 bundle_prefix: &str,
546 default_function: &str,
547) -> Result<(), BenchError> {
548 let target_dir = output_dir.join("ios");
549 reset_generated_project_dir(&target_dir)?;
550 let sanitized_bundle_prefix = {
553 let parts: Vec<&str> = bundle_prefix.split('.').collect();
554 parts
555 .iter()
556 .map(|part| sanitize_bundle_id_component(part))
557 .collect::<Vec<_>>()
558 .join(".")
559 };
560 let vars = vec![
564 TemplateVar {
565 name: "DEFAULT_FUNCTION",
566 value: default_function.to_string(),
567 },
568 TemplateVar {
569 name: "PROJECT_NAME_PASCAL",
570 value: project_pascal.to_string(),
571 },
572 TemplateVar {
573 name: "BUNDLE_ID_PREFIX",
574 value: sanitized_bundle_prefix.clone(),
575 },
576 TemplateVar {
577 name: "BUNDLE_ID",
578 value: format!("{}.{}", sanitized_bundle_prefix, project_pascal),
579 },
580 TemplateVar {
581 name: "LIBRARY_NAME",
582 value: project_slug.replace('-', "_"),
583 },
584 ];
585 render_dir(&IOS_TEMPLATES, &target_dir, &vars)?;
586 Ok(())
587}
588
589fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
591 let config_target = match config.target {
592 Target::Ios => "ios",
593 Target::Android | Target::Both => "android",
594 };
595 let config_content = format!(
596 r#"# mobench configuration
597# This file controls how benchmarks are executed on devices.
598
599target = "{}"
600function = "example_fibonacci"
601iterations = 100
602warmup = 10
603device_matrix = "device-matrix.yaml"
604device_tags = ["default"]
605
606[browserstack]
607app_automate_username = "${{BROWSERSTACK_USERNAME}}"
608app_automate_access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
609project = "{}-benchmarks"
610
611[ios_xcuitest]
612app = "target/ios/BenchRunner.ipa"
613test_suite = "target/ios/BenchRunnerUITests.zip"
614"#,
615 config_target, config.project_name
616 );
617
618 fs::write(output_dir.join("bench-config.toml"), config_content)?;
619
620 Ok(())
621}
622
623fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
625 let examples_dir = output_dir.join("benches");
626 fs::create_dir_all(&examples_dir)?;
627
628 let example_content = r#"//! Example benchmarks
629//!
630//! This file demonstrates how to write benchmarks with mobench-sdk.
631
632use mobench_sdk::benchmark;
633
634/// Simple benchmark example
635#[benchmark]
636fn example_fibonacci() {
637 let result = fibonacci(30);
638 std::hint::black_box(result);
639}
640
641/// Another example with a loop
642#[benchmark]
643fn example_sum() {
644 let mut sum = 0u64;
645 for i in 0..10000 {
646 sum = sum.wrapping_add(i);
647 }
648 std::hint::black_box(sum);
649}
650
651// Helper function (not benchmarked)
652fn fibonacci(n: u32) -> u64 {
653 match n {
654 0 => 0,
655 1 => 1,
656 _ => {
657 let mut a = 0u64;
658 let mut b = 1u64;
659 for _ in 2..=n {
660 let next = a.wrapping_add(b);
661 a = b;
662 b = next;
663 }
664 b
665 }
666 }
667}
668"#;
669
670 fs::write(examples_dir.join("example.rs"), example_content)?;
671
672 Ok(())
673}
674
675const TEMPLATE_EXTENSIONS: &[&str] = &[
677 "gradle",
678 "xml",
679 "kt",
680 "java",
681 "swift",
682 "yml",
683 "yaml",
684 "json",
685 "toml",
686 "md",
687 "txt",
688 "h",
689 "m",
690 "plist",
691 "pbxproj",
692 "xcscheme",
693 "xcworkspacedata",
694 "entitlements",
695 "modulemap",
696];
697
698fn render_dir(dir: &Dir, out_root: &Path, vars: &[TemplateVar]) -> Result<(), BenchError> {
699 for entry in dir.entries() {
700 match entry {
701 DirEntry::Dir(sub) => {
702 if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
704 continue;
705 }
706 render_dir(sub, out_root, vars)?;
707 }
708 DirEntry::File(file) => {
709 if file.path().components().any(|c| c.as_os_str() == ".gradle") {
710 continue;
711 }
712 let mut relative = file.path().to_path_buf();
714 let mut contents = file.contents().to_vec();
715
716 let is_explicit_template = relative
718 .extension()
719 .map(|ext| ext == "template")
720 .unwrap_or(false);
721
722 let should_render = is_explicit_template || is_template_file(&relative);
724
725 if is_explicit_template {
726 relative.set_extension("");
728 }
729
730 if should_render {
731 if let Ok(text) = std::str::from_utf8(&contents) {
732 let rendered = render_template(text, vars);
733 validate_no_unreplaced_placeholders(&rendered, &relative)?;
735 contents = rendered.into_bytes();
736 }
737 }
738
739 let out_path = out_root.join(relative);
740 if let Some(parent) = out_path.parent() {
741 fs::create_dir_all(parent)?;
742 }
743 fs::write(&out_path, contents)?;
744 }
745 }
746 }
747 Ok(())
748}
749
750fn is_template_file(path: &Path) -> bool {
753 if let Some(ext) = path.extension() {
755 if ext == "template" {
756 return true;
757 }
758 if let Some(ext_str) = ext.to_str() {
760 return TEMPLATE_EXTENSIONS.contains(&ext_str);
761 }
762 }
763 if let Some(stem) = path.file_stem() {
765 let stem_path = Path::new(stem);
766 if let Some(ext) = stem_path.extension() {
767 if let Some(ext_str) = ext.to_str() {
768 return TEMPLATE_EXTENSIONS.contains(&ext_str);
769 }
770 }
771 }
772 false
773}
774
775fn validate_no_unreplaced_placeholders(content: &str, file_path: &Path) -> Result<(), BenchError> {
777 let mut pos = 0;
779 let mut unreplaced = Vec::new();
780
781 while let Some(start) = content[pos..].find("{{") {
782 let abs_start = pos + start;
783 if let Some(end) = content[abs_start..].find("}}") {
784 let placeholder = &content[abs_start..abs_start + end + 2];
785 let var_name = &content[abs_start + 2..abs_start + end];
787 if !var_name.contains('$') && !var_name.contains(' ') && !var_name.is_empty() {
790 unreplaced.push(placeholder.to_string());
791 }
792 pos = abs_start + end + 2;
793 } else {
794 break;
795 }
796 }
797
798 if !unreplaced.is_empty() {
799 return Err(BenchError::Build(format!(
800 "Template validation failed for {:?}: unreplaced placeholders found: {:?}\n\n\
801 This is a bug in mobench-sdk. Please report it at:\n\
802 https://github.com/worldcoin/mobile-bench-rs/issues",
803 file_path, unreplaced
804 )));
805 }
806
807 Ok(())
808}
809
810fn render_template(input: &str, vars: &[TemplateVar]) -> String {
811 let mut output = input.to_string();
812 for var in vars {
813 output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
814 }
815 output
816}
817
818pub fn sanitize_bundle_id_component(name: &str) -> String {
829 name.chars()
830 .filter(|c| c.is_ascii_alphanumeric())
831 .collect::<String>()
832 .to_lowercase()
833}
834
835fn sanitize_package_name(name: &str) -> String {
836 name.chars()
837 .map(|c| {
838 if c.is_ascii_alphanumeric() {
839 c.to_ascii_lowercase()
840 } else {
841 '-'
842 }
843 })
844 .collect::<String>()
845 .trim_matches('-')
846 .replace("--", "-")
847}
848
849pub fn to_pascal_case(input: &str) -> String {
851 input
852 .split(|c: char| !c.is_ascii_alphanumeric())
853 .filter(|s| !s.is_empty())
854 .map(|s| {
855 let mut chars = s.chars();
856 let first = chars.next().unwrap().to_ascii_uppercase();
857 let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
858 format!("{}{}", first, rest)
859 })
860 .collect::<String>()
861}
862
863pub fn android_project_exists(output_dir: &Path) -> bool {
867 let android_dir = output_dir.join("android");
868 android_dir.join("build.gradle").exists() || android_dir.join("build.gradle.kts").exists()
869}
870
871pub fn ios_project_exists(output_dir: &Path) -> bool {
875 output_dir.join("ios/BenchRunner/project.yml").exists()
876}
877
878fn ios_project_matches_library(output_dir: &Path, library_name: &str) -> bool {
883 let project_yml = output_dir.join("ios/BenchRunner/project.yml");
884 let Ok(content) = std::fs::read_to_string(&project_yml) else {
885 return false;
886 };
887 let expected = format!("../{}.xcframework", library_name);
888 content.contains(&expected)
889}
890
891fn android_project_matches_library(output_dir: &Path, library_name: &str) -> bool {
896 let build_gradle = output_dir.join("android/app/build.gradle");
897 let Ok(content) = std::fs::read_to_string(&build_gradle) else {
898 return false;
899 };
900 let expected = format!("lib{}.so", library_name);
901 content.contains(&expected)
902}
903
904pub fn detect_default_function(crate_dir: &Path, crate_name: &str) -> Option<String> {
919 let lib_rs = crate_dir.join("src/lib.rs");
920 if !lib_rs.exists() {
921 return None;
922 }
923
924 let file = fs::File::open(&lib_rs).ok()?;
925 let reader = BufReader::new(file);
926
927 let mut found_benchmark_attr = false;
928 let crate_name_normalized = crate_name.replace('-', "_");
929
930 for line in reader.lines().map_while(Result::ok) {
931 let trimmed = line.trim();
932
933 if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
935 found_benchmark_attr = true;
936 continue;
937 }
938
939 if found_benchmark_attr {
941 if let Some(fn_pos) = trimmed.find("fn ") {
943 let after_fn = &trimmed[fn_pos + 3..];
944 let fn_name: String = after_fn
946 .chars()
947 .take_while(|c| c.is_alphanumeric() || *c == '_')
948 .collect();
949
950 if !fn_name.is_empty() {
951 return Some(format!("{}::{}", crate_name_normalized, fn_name));
952 }
953 }
954 if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
957 found_benchmark_attr = false;
958 }
959 }
960 }
961
962 None
963}
964
965pub fn detect_all_benchmarks(crate_dir: &Path, crate_name: &str) -> Vec<String> {
979 let lib_rs = crate_dir.join("src/lib.rs");
980 if !lib_rs.exists() {
981 return Vec::new();
982 }
983
984 let Ok(file) = fs::File::open(&lib_rs) else {
985 return Vec::new();
986 };
987 let reader = BufReader::new(file);
988
989 let mut benchmarks = Vec::new();
990 let mut found_benchmark_attr = false;
991 let crate_name_normalized = crate_name.replace('-', "_");
992
993 for line in reader.lines().map_while(Result::ok) {
994 let trimmed = line.trim();
995
996 if trimmed == "#[benchmark]" || trimmed.starts_with("#[benchmark(") {
998 found_benchmark_attr = true;
999 continue;
1000 }
1001
1002 if found_benchmark_attr {
1004 if let Some(fn_pos) = trimmed.find("fn ") {
1006 let after_fn = &trimmed[fn_pos + 3..];
1007 let fn_name: String = after_fn
1009 .chars()
1010 .take_while(|c| c.is_alphanumeric() || *c == '_')
1011 .collect();
1012
1013 if !fn_name.is_empty() {
1014 benchmarks.push(format!("{}::{}", crate_name_normalized, fn_name));
1015 }
1016 found_benchmark_attr = false;
1017 }
1018 if !trimmed.starts_with('#') && !trimmed.starts_with("//") && !trimmed.is_empty() {
1021 found_benchmark_attr = false;
1022 }
1023 }
1024 }
1025
1026 benchmarks
1027}
1028
1029pub fn validate_benchmark_exists(crate_dir: &Path, crate_name: &str, function_name: &str) -> bool {
1041 let benchmarks = detect_all_benchmarks(crate_dir, crate_name);
1042 let crate_name_normalized = crate_name.replace('-', "_");
1043
1044 let normalized_name = if function_name.contains("::") {
1046 function_name.to_string()
1047 } else {
1048 format!("{}::{}", crate_name_normalized, function_name)
1049 };
1050
1051 benchmarks.iter().any(|b| b == &normalized_name)
1052}
1053
1054pub fn resolve_default_function(
1069 project_root: &Path,
1070 crate_name: &str,
1071 crate_dir: Option<&Path>,
1072) -> String {
1073 let crate_name_normalized = crate_name.replace('-', "_");
1074
1075 let search_dirs: Vec<PathBuf> = if let Some(dir) = crate_dir {
1077 vec![dir.to_path_buf()]
1078 } else {
1079 vec![
1080 project_root.join("bench-mobile"),
1081 project_root.join("crates").join(crate_name),
1082 project_root.to_path_buf(),
1083 ]
1084 };
1085
1086 for dir in &search_dirs {
1088 if dir.join("Cargo.toml").exists() {
1089 if let Some(detected) = detect_default_function(dir, &crate_name_normalized) {
1090 return detected;
1091 }
1092 }
1093 }
1094
1095 format!("{}::example_benchmark", crate_name_normalized)
1097}
1098
1099pub fn ensure_android_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
1110 ensure_android_project_with_options(output_dir, crate_name, None, None)
1111}
1112
1113pub fn ensure_android_project_with_options(
1125 output_dir: &Path,
1126 crate_name: &str,
1127 project_root: Option<&Path>,
1128 crate_dir: Option<&Path>,
1129) -> Result<(), BenchError> {
1130 let library_name = crate_name.replace('-', "_");
1131 if android_project_exists(output_dir)
1132 && android_project_matches_library(output_dir, &library_name)
1133 {
1134 return Ok(());
1135 }
1136
1137 println!("Android project not found, generating scaffolding...");
1138 let project_slug = crate_name.replace('-', "_");
1139
1140 let effective_root = project_root.unwrap_or_else(|| output_dir.parent().unwrap_or(output_dir));
1142 let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
1143
1144 generate_android_project(output_dir, &project_slug, &default_function)?;
1145 println!(
1146 " Generated Android project at {:?}",
1147 output_dir.join("android")
1148 );
1149 println!(" Default benchmark function: {}", default_function);
1150 Ok(())
1151}
1152
1153pub fn ensure_ios_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
1164 ensure_ios_project_with_options(output_dir, crate_name, None, None)
1165}
1166
1167pub fn ensure_ios_project_with_options(
1179 output_dir: &Path,
1180 crate_name: &str,
1181 project_root: Option<&Path>,
1182 crate_dir: Option<&Path>,
1183) -> Result<(), BenchError> {
1184 let library_name = crate_name.replace('-', "_");
1185 let project_exists = ios_project_exists(output_dir);
1186 let project_matches = ios_project_matches_library(output_dir, &library_name);
1187 if project_exists && !project_matches {
1188 println!("Existing iOS scaffolding does not match library, regenerating...");
1189 } else if project_exists {
1190 println!("Refreshing generated iOS scaffolding...");
1191 } else {
1192 println!("iOS project not found, generating scaffolding...");
1193 }
1194
1195 let project_pascal = "BenchRunner";
1197 let library_name = crate_name.replace('-', "_");
1199 let bundle_id_component = sanitize_bundle_id_component(crate_name);
1202 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1203
1204 let effective_root = project_root.unwrap_or_else(|| output_dir.parent().unwrap_or(output_dir));
1206 let default_function = resolve_default_function(effective_root, crate_name, crate_dir);
1207
1208 generate_ios_project(
1209 output_dir,
1210 &library_name,
1211 project_pascal,
1212 &bundle_prefix,
1213 &default_function,
1214 )?;
1215 println!(" Generated iOS project at {:?}", output_dir.join("ios"));
1216 println!(" Default benchmark function: {}", default_function);
1217 Ok(())
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223 use std::env;
1224
1225 #[test]
1226 fn test_generate_bench_mobile_crate() {
1227 let temp_dir = env::temp_dir().join("mobench-sdk-test");
1228 fs::create_dir_all(&temp_dir).unwrap();
1229
1230 let result = generate_bench_mobile_crate(&temp_dir, "test_project");
1231 assert!(result.is_ok());
1232
1233 assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
1235 assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
1236 assert!(temp_dir.join("bench-mobile/build.rs").exists());
1237
1238 fs::remove_dir_all(&temp_dir).ok();
1240 }
1241
1242 #[test]
1243 fn test_generate_android_project_no_unreplaced_placeholders() {
1244 let temp_dir = env::temp_dir().join("mobench-sdk-android-test");
1245 let _ = fs::remove_dir_all(&temp_dir);
1247 fs::create_dir_all(&temp_dir).unwrap();
1248
1249 let result =
1250 generate_android_project(&temp_dir, "my-bench-project", "my_bench_project::test_func");
1251 assert!(
1252 result.is_ok(),
1253 "generate_android_project failed: {:?}",
1254 result.err()
1255 );
1256
1257 let android_dir = temp_dir.join("android");
1259 assert!(android_dir.join("settings.gradle").exists());
1260 assert!(android_dir.join("app/build.gradle").exists());
1261 assert!(
1262 android_dir
1263 .join("app/src/main/AndroidManifest.xml")
1264 .exists()
1265 );
1266 assert!(
1267 android_dir
1268 .join("app/src/main/res/values/strings.xml")
1269 .exists()
1270 );
1271 assert!(
1272 android_dir
1273 .join("app/src/main/res/values/themes.xml")
1274 .exists()
1275 );
1276
1277 let files_to_check = [
1279 "settings.gradle",
1280 "app/build.gradle",
1281 "app/src/main/AndroidManifest.xml",
1282 "app/src/main/res/values/strings.xml",
1283 "app/src/main/res/values/themes.xml",
1284 ];
1285
1286 for file in files_to_check {
1287 let path = android_dir.join(file);
1288 let contents = fs::read_to_string(&path).expect(&format!("Failed to read {}", file));
1289
1290 let has_placeholder = contents.contains("{{") && contents.contains("}}");
1292 assert!(
1293 !has_placeholder,
1294 "File {} contains unreplaced template placeholders: {}",
1295 file, contents
1296 );
1297 }
1298
1299 let settings = fs::read_to_string(android_dir.join("settings.gradle")).unwrap();
1301 assert!(
1302 settings.contains("my-bench-project-android")
1303 || settings.contains("my_bench_project-android"),
1304 "settings.gradle should contain project name"
1305 );
1306
1307 let build_gradle = fs::read_to_string(android_dir.join("app/build.gradle")).unwrap();
1308 assert!(
1310 build_gradle.contains("dev.world.mybenchproject"),
1311 "build.gradle should contain sanitized package name 'dev.world.mybenchproject'"
1312 );
1313
1314 let manifest =
1315 fs::read_to_string(android_dir.join("app/src/main/AndroidManifest.xml")).unwrap();
1316 assert!(
1317 manifest.contains("Theme.MyBenchProject"),
1318 "AndroidManifest.xml should contain PascalCase theme name"
1319 );
1320
1321 let strings =
1322 fs::read_to_string(android_dir.join("app/src/main/res/values/strings.xml")).unwrap();
1323 assert!(
1324 strings.contains("Benchmark"),
1325 "strings.xml should contain app name with Benchmark"
1326 );
1327
1328 let main_activity_path =
1331 android_dir.join("app/src/main/java/dev/world/mybenchproject/MainActivity.kt");
1332 assert!(
1333 main_activity_path.exists(),
1334 "MainActivity.kt should be in package directory: {:?}",
1335 main_activity_path
1336 );
1337
1338 let test_activity_path = android_dir
1339 .join("app/src/androidTest/java/dev/world/mybenchproject/MainActivityTest.kt");
1340 assert!(
1341 test_activity_path.exists(),
1342 "MainActivityTest.kt should be in package directory: {:?}",
1343 test_activity_path
1344 );
1345
1346 assert!(
1348 !android_dir
1349 .join("app/src/main/java/MainActivity.kt")
1350 .exists(),
1351 "MainActivity.kt should not be in root java directory"
1352 );
1353 assert!(
1354 !android_dir
1355 .join("app/src/androidTest/java/MainActivityTest.kt")
1356 .exists(),
1357 "MainActivityTest.kt should not be in root java directory"
1358 );
1359
1360 fs::remove_dir_all(&temp_dir).ok();
1362 }
1363
1364 #[test]
1365 fn test_generate_android_project_replaces_previous_package_tree() {
1366 let temp_dir = env::temp_dir().join("mobench-sdk-android-regenerate-test");
1367 let _ = fs::remove_dir_all(&temp_dir);
1368 fs::create_dir_all(&temp_dir).unwrap();
1369
1370 generate_android_project(&temp_dir, "ffi_benchmark", "ffi_benchmark::bench_fibonacci")
1371 .unwrap();
1372 let old_package_dir = temp_dir.join("android/app/src/main/java/dev/world/ffibenchmark");
1373 assert!(
1374 old_package_dir.exists(),
1375 "expected first package tree to exist"
1376 );
1377
1378 generate_android_project(
1379 &temp_dir,
1380 "basic_benchmark",
1381 "basic_benchmark::bench_fibonacci",
1382 )
1383 .unwrap();
1384
1385 let new_package_dir = temp_dir.join("android/app/src/main/java/dev/world/basicbenchmark");
1386 assert!(
1387 new_package_dir.exists(),
1388 "expected new package tree to exist"
1389 );
1390 assert!(
1391 !old_package_dir.exists(),
1392 "old package tree should be removed when regenerating the Android scaffold"
1393 );
1394
1395 fs::remove_dir_all(&temp_dir).ok();
1396 }
1397
1398 #[test]
1399 fn test_generate_android_project_preserves_existing_assets_on_regeneration() {
1400 let temp_dir = env::temp_dir().join("mobench-sdk-android-assets-regenerate-test");
1401 let _ = fs::remove_dir_all(&temp_dir);
1402 fs::create_dir_all(&temp_dir).unwrap();
1403
1404 generate_android_project(&temp_dir, "ffi_benchmark", "ffi_benchmark::bench_fibonacci")
1405 .unwrap();
1406
1407 let assets_dir = temp_dir.join("android/app/src/main/assets");
1408 fs::create_dir_all(assets_dir.join("nested")).unwrap();
1409 fs::write(
1410 assets_dir.join("bench_spec.json"),
1411 r#"{"function":"ffi_benchmark::bench_fibonacci"}"#,
1412 )
1413 .unwrap();
1414 fs::write(
1415 assets_dir.join("bench_meta.json"),
1416 r#"{"build_id":"build-123"}"#,
1417 )
1418 .unwrap();
1419 fs::write(assets_dir.join("nested/custom.txt"), "keep me").unwrap();
1420
1421 generate_android_project(
1422 &temp_dir,
1423 "basic_benchmark",
1424 "basic_benchmark::bench_fibonacci",
1425 )
1426 .unwrap();
1427
1428 assert_eq!(
1429 fs::read_to_string(assets_dir.join("bench_spec.json")).unwrap(),
1430 r#"{"function":"ffi_benchmark::bench_fibonacci"}"#
1431 );
1432 assert_eq!(
1433 fs::read_to_string(assets_dir.join("bench_meta.json")).unwrap(),
1434 r#"{"build_id":"build-123"}"#
1435 );
1436 assert_eq!(
1437 fs::read_to_string(assets_dir.join("nested/custom.txt")).unwrap(),
1438 "keep me"
1439 );
1440
1441 fs::remove_dir_all(&temp_dir).ok();
1442 }
1443
1444 #[test]
1445 fn test_is_template_file() {
1446 assert!(is_template_file(Path::new("settings.gradle")));
1447 assert!(is_template_file(Path::new("app/build.gradle")));
1448 assert!(is_template_file(Path::new("AndroidManifest.xml")));
1449 assert!(is_template_file(Path::new("strings.xml")));
1450 assert!(is_template_file(Path::new("MainActivity.kt.template")));
1451 assert!(is_template_file(Path::new("project.yml")));
1452 assert!(is_template_file(Path::new("Info.plist")));
1453 assert!(!is_template_file(Path::new("libfoo.so")));
1454 assert!(!is_template_file(Path::new("image.png")));
1455 }
1456
1457 #[test]
1458 fn test_validate_no_unreplaced_placeholders() {
1459 assert!(validate_no_unreplaced_placeholders("hello world", Path::new("test.txt")).is_ok());
1461
1462 assert!(validate_no_unreplaced_placeholders("${ENV_VAR}", Path::new("test.txt")).is_ok());
1464
1465 let result = validate_no_unreplaced_placeholders("hello {{NAME}}", Path::new("test.txt"));
1467 assert!(result.is_err());
1468 let err = result.unwrap_err().to_string();
1469 assert!(err.contains("{{NAME}}"));
1470 }
1471
1472 #[test]
1473 fn test_to_pascal_case() {
1474 assert_eq!(to_pascal_case("my-project"), "MyProject");
1475 assert_eq!(to_pascal_case("my_project"), "MyProject");
1476 assert_eq!(to_pascal_case("myproject"), "Myproject");
1477 assert_eq!(to_pascal_case("my-bench-project"), "MyBenchProject");
1478 }
1479
1480 #[test]
1481 fn test_detect_default_function_finds_benchmark() {
1482 let temp_dir = env::temp_dir().join("mobench-sdk-detect-test");
1483 let _ = fs::remove_dir_all(&temp_dir);
1484 fs::create_dir_all(temp_dir.join("src")).unwrap();
1485
1486 let lib_content = r#"
1488use mobench_sdk::benchmark;
1489
1490/// Some docs
1491#[benchmark]
1492fn my_benchmark_func() {
1493 // benchmark code
1494}
1495
1496fn helper_func() {}
1497"#;
1498 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1499 fs::write(temp_dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
1500
1501 let result = detect_default_function(&temp_dir, "my_crate");
1502 assert_eq!(result, Some("my_crate::my_benchmark_func".to_string()));
1503
1504 fs::remove_dir_all(&temp_dir).ok();
1506 }
1507
1508 #[test]
1509 fn test_detect_default_function_no_benchmark() {
1510 let temp_dir = env::temp_dir().join("mobench-sdk-detect-none-test");
1511 let _ = fs::remove_dir_all(&temp_dir);
1512 fs::create_dir_all(temp_dir.join("src")).unwrap();
1513
1514 let lib_content = r#"
1516fn regular_function() {
1517 // no benchmark here
1518}
1519"#;
1520 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1521
1522 let result = detect_default_function(&temp_dir, "my_crate");
1523 assert!(result.is_none());
1524
1525 fs::remove_dir_all(&temp_dir).ok();
1527 }
1528
1529 #[test]
1530 fn test_detect_default_function_pub_fn() {
1531 let temp_dir = env::temp_dir().join("mobench-sdk-detect-pub-test");
1532 let _ = fs::remove_dir_all(&temp_dir);
1533 fs::create_dir_all(temp_dir.join("src")).unwrap();
1534
1535 let lib_content = r#"
1537#[benchmark]
1538pub fn public_bench() {
1539 // benchmark code
1540}
1541"#;
1542 fs::write(temp_dir.join("src/lib.rs"), lib_content).unwrap();
1543
1544 let result = detect_default_function(&temp_dir, "test-crate");
1545 assert_eq!(result, Some("test_crate::public_bench".to_string()));
1546
1547 fs::remove_dir_all(&temp_dir).ok();
1549 }
1550
1551 #[test]
1552 fn test_resolve_default_function_fallback() {
1553 let temp_dir = env::temp_dir().join("mobench-sdk-resolve-test");
1554 let _ = fs::remove_dir_all(&temp_dir);
1555 fs::create_dir_all(&temp_dir).unwrap();
1556
1557 let result = resolve_default_function(&temp_dir, "my-crate", None);
1559 assert_eq!(result, "my_crate::example_benchmark");
1560
1561 fs::remove_dir_all(&temp_dir).ok();
1563 }
1564
1565 #[test]
1566 fn test_sanitize_bundle_id_component() {
1567 assert_eq!(sanitize_bundle_id_component("bench-mobile"), "benchmobile");
1569 assert_eq!(sanitize_bundle_id_component("bench_mobile"), "benchmobile");
1571 assert_eq!(
1573 sanitize_bundle_id_component("my-project_name"),
1574 "myprojectname"
1575 );
1576 assert_eq!(sanitize_bundle_id_component("benchmobile"), "benchmobile");
1578 assert_eq!(sanitize_bundle_id_component("bench2mobile"), "bench2mobile");
1580 assert_eq!(sanitize_bundle_id_component("BenchMobile"), "benchmobile");
1582 assert_eq!(
1584 sanitize_bundle_id_component("My-Complex_Project-123"),
1585 "mycomplexproject123"
1586 );
1587 }
1588
1589 #[test]
1590 fn test_generate_ios_project_bundle_id_not_duplicated() {
1591 let temp_dir = env::temp_dir().join("mobench-sdk-ios-bundle-test");
1592 let _ = fs::remove_dir_all(&temp_dir);
1594 fs::create_dir_all(&temp_dir).unwrap();
1595
1596 let crate_name = "bench-mobile";
1598 let bundle_prefix = "dev.world.benchmobile";
1599 let project_pascal = "BenchRunner";
1600
1601 let result = generate_ios_project(
1602 &temp_dir,
1603 crate_name,
1604 project_pascal,
1605 bundle_prefix,
1606 "bench_mobile::test_func",
1607 );
1608 assert!(
1609 result.is_ok(),
1610 "generate_ios_project failed: {:?}",
1611 result.err()
1612 );
1613
1614 let project_yml_path = temp_dir.join("ios/BenchRunner/project.yml");
1616 assert!(project_yml_path.exists(), "project.yml should exist");
1617
1618 let project_yml = fs::read_to_string(&project_yml_path).unwrap();
1620
1621 assert!(
1624 project_yml.contains("dev.world.benchmobile.BenchRunner"),
1625 "Bundle ID should be 'dev.world.benchmobile.BenchRunner', got:\n{}",
1626 project_yml
1627 );
1628 assert!(
1629 !project_yml.contains("dev.world.benchmobile.benchmobile"),
1630 "Bundle ID should NOT be duplicated as 'dev.world.benchmobile.benchmobile', got:\n{}",
1631 project_yml
1632 );
1633 assert!(
1634 project_yml.contains("embed: false"),
1635 "Static xcframework dependency should be link-only, got:\n{}",
1636 project_yml
1637 );
1638
1639 fs::remove_dir_all(&temp_dir).ok();
1641 }
1642
1643 #[test]
1644 fn test_ensure_ios_project_refreshes_existing_content_view_template() {
1645 let temp_dir = env::temp_dir().join("mobench-sdk-ios-refresh-test");
1646 let _ = fs::remove_dir_all(&temp_dir);
1647 fs::create_dir_all(&temp_dir).unwrap();
1648
1649 ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
1650 .expect("initial iOS project generation should succeed");
1651
1652 let content_view_path = temp_dir.join("ios/BenchRunner/BenchRunner/ContentView.swift");
1653 assert!(content_view_path.exists(), "ContentView.swift should exist");
1654
1655 fs::write(&content_view_path, "stale generated content").unwrap();
1656
1657 ensure_ios_project_with_options(&temp_dir, "sample-fns", None, None)
1658 .expect("refreshing existing iOS project should succeed");
1659
1660 let refreshed = fs::read_to_string(&content_view_path).unwrap();
1661 assert!(
1662 refreshed.contains("ProfileLaunchOptions"),
1663 "refreshed ContentView.swift should contain the latest profiling template, got:\n{}",
1664 refreshed
1665 );
1666 assert!(
1667 refreshed.contains("repeatUntilMs"),
1668 "refreshed ContentView.swift should contain repeat-until profiling support, got:\n{}",
1669 refreshed
1670 );
1671
1672 fs::remove_dir_all(&temp_dir).ok();
1673 }
1674
1675 #[test]
1676 fn test_cross_platform_naming_consistency() {
1677 let temp_dir = env::temp_dir().join("mobench-sdk-naming-consistency-test");
1679 let _ = fs::remove_dir_all(&temp_dir);
1680 fs::create_dir_all(&temp_dir).unwrap();
1681
1682 let project_name = "bench-mobile";
1683
1684 let result = generate_android_project(&temp_dir, project_name, "bench_mobile::test_func");
1686 assert!(
1687 result.is_ok(),
1688 "generate_android_project failed: {:?}",
1689 result.err()
1690 );
1691
1692 let bundle_id_component = sanitize_bundle_id_component(project_name);
1694 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1695 let result = generate_ios_project(
1696 &temp_dir,
1697 &project_name.replace('-', "_"),
1698 "BenchRunner",
1699 &bundle_prefix,
1700 "bench_mobile::test_func",
1701 );
1702 assert!(
1703 result.is_ok(),
1704 "generate_ios_project failed: {:?}",
1705 result.err()
1706 );
1707
1708 let android_build_gradle = fs::read_to_string(temp_dir.join("android/app/build.gradle"))
1710 .expect("Failed to read Android build.gradle");
1711
1712 let ios_project_yml = fs::read_to_string(temp_dir.join("ios/BenchRunner/project.yml"))
1714 .expect("Failed to read iOS project.yml");
1715
1716 assert!(
1720 android_build_gradle.contains("dev.world.benchmobile"),
1721 "Android package should be 'dev.world.benchmobile', got:\n{}",
1722 android_build_gradle
1723 );
1724 assert!(
1725 ios_project_yml.contains("dev.world.benchmobile"),
1726 "iOS bundle prefix should contain 'dev.world.benchmobile', got:\n{}",
1727 ios_project_yml
1728 );
1729
1730 assert!(
1732 !android_build_gradle.contains("dev.world.bench-mobile"),
1733 "Android package should NOT contain hyphens"
1734 );
1735 assert!(
1736 !android_build_gradle.contains("dev.world.bench_mobile"),
1737 "Android package should NOT contain underscores"
1738 );
1739
1740 fs::remove_dir_all(&temp_dir).ok();
1742 }
1743
1744 #[test]
1745 fn test_cross_platform_version_consistency() {
1746 let temp_dir = env::temp_dir().join("mobench-sdk-version-consistency-test");
1748 let _ = fs::remove_dir_all(&temp_dir);
1749 fs::create_dir_all(&temp_dir).unwrap();
1750
1751 let project_name = "test-project";
1752
1753 let result = generate_android_project(&temp_dir, project_name, "test_project::test_func");
1755 assert!(
1756 result.is_ok(),
1757 "generate_android_project failed: {:?}",
1758 result.err()
1759 );
1760
1761 let bundle_id_component = sanitize_bundle_id_component(project_name);
1763 let bundle_prefix = format!("dev.world.{}", bundle_id_component);
1764 let result = generate_ios_project(
1765 &temp_dir,
1766 &project_name.replace('-', "_"),
1767 "BenchRunner",
1768 &bundle_prefix,
1769 "test_project::test_func",
1770 );
1771 assert!(
1772 result.is_ok(),
1773 "generate_ios_project failed: {:?}",
1774 result.err()
1775 );
1776
1777 let android_build_gradle = fs::read_to_string(temp_dir.join("android/app/build.gradle"))
1779 .expect("Failed to read Android build.gradle");
1780
1781 let ios_project_yml = fs::read_to_string(temp_dir.join("ios/BenchRunner/project.yml"))
1783 .expect("Failed to read iOS project.yml");
1784
1785 assert!(
1787 android_build_gradle.contains("versionName \"1.0.0\""),
1788 "Android versionName should be '1.0.0', got:\n{}",
1789 android_build_gradle
1790 );
1791 assert!(
1792 ios_project_yml.contains("CFBundleShortVersionString: \"1.0.0\""),
1793 "iOS CFBundleShortVersionString should be '1.0.0', got:\n{}",
1794 ios_project_yml
1795 );
1796
1797 fs::remove_dir_all(&temp_dir).ok();
1799 }
1800
1801 #[test]
1802 fn test_bundle_id_prefix_consistency() {
1803 let test_cases = vec![
1805 ("my-project", "dev.world.myproject"),
1806 ("bench_mobile", "dev.world.benchmobile"),
1807 ("TestApp", "dev.world.testapp"),
1808 ("app-with-many-dashes", "dev.world.appwithmanydashes"),
1809 (
1810 "app_with_many_underscores",
1811 "dev.world.appwithmanyunderscores",
1812 ),
1813 ];
1814
1815 for (input, expected_prefix) in test_cases {
1816 let sanitized = sanitize_bundle_id_component(input);
1817 let full_prefix = format!("dev.world.{}", sanitized);
1818 assert_eq!(
1819 full_prefix, expected_prefix,
1820 "For input '{}', expected '{}' but got '{}'",
1821 input, expected_prefix, full_prefix
1822 );
1823 }
1824 }
1825}