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