@@ -1245,17 +1245,44 @@ pub fn run() -> Result<()> {
12451245 println ! ( "No BrowserStack run to fetch (devices not provided?)" ) ;
12461246 }
12471247
1248+ let mut baseline_compare_path = None ;
1249+ let mut baseline_snapshot_path = None ;
1250+ if let Some ( baseline_source) = baseline. as_deref ( ) {
1251+ let resolved_baseline = resolve_baseline_source ( baseline_source) ?;
1252+ if paths_point_to_same_file ( & resolved_baseline, & summary_paths. json ) ? {
1253+ if !resolved_baseline. exists ( ) {
1254+ bail ! (
1255+ "config_error: baseline source `{}` resolves to output path {}; provide an existing baseline file or a different path" ,
1256+ baseline_source,
1257+ summary_paths. json. display( )
1258+ ) ;
1259+ }
1260+ let snapshot = snapshot_baseline_for_compare ( & resolved_baseline) ?;
1261+ baseline_snapshot_path = Some ( snapshot. clone ( ) ) ;
1262+ baseline_compare_path = Some ( snapshot) ;
1263+ } else {
1264+ baseline_compare_path = Some ( resolved_baseline) ;
1265+ }
1266+ }
1267+
12481268 run_summary. summary = build_summary ( & run_summary) ?;
12491269 write_summary ( & run_summary, & summary_paths, summary_csv) ?;
12501270
12511271 let mut compare_report = None ;
12521272 let mut regression_findings: Vec < RegressionFinding > = Vec :: new ( ) ;
1253- if let Some ( baseline_source) = baseline. as_deref ( ) {
1254- let baseline_path = resolve_baseline_source ( baseline_source) ?;
1273+ if let Some ( baseline_path) = baseline_compare_path. as_deref ( ) {
12551274 let report = compare_summaries ( & baseline_path, & summary_paths. json ) ?;
12561275 regression_findings = detect_regressions ( & report, regression_threshold_pct) ;
12571276 compare_report = Some ( report) ;
12581277 }
1278+ if let Some ( snapshot_path) = baseline_snapshot_path {
1279+ if let Err ( err) = fs:: remove_file ( & snapshot_path) {
1280+ eprintln ! (
1281+ "Warning: failed to remove baseline snapshot {}: {err}" ,
1282+ snapshot_path. display( )
1283+ ) ;
1284+ }
1285+ }
12591286 if let Some ( report) = & compare_report {
12601287 inject_compare_into_summary (
12611288 & summary_paths. json ,
@@ -2450,7 +2477,8 @@ fn resolve_run_spec(
24502477) -> Result < RunSpec > {
24512478 if let Some ( cfg_path) = config {
24522479 let cfg = load_config ( cfg_path) ?;
2453- let matrix = load_device_matrix ( & cfg. device_matrix ) ?;
2480+ let matrix_path = device_matrix. unwrap_or ( cfg. device_matrix . as_path ( ) ) ;
2481+ let matrix = load_device_matrix ( matrix_path) ?;
24542482 let resolved_tags = if !device_tags. is_empty ( ) {
24552483 Some ( device_tags)
24562484 } else {
@@ -3717,6 +3745,37 @@ fn resolve_baseline_source(source: &str) -> Result<PathBuf> {
37173745 Ok ( PathBuf :: from ( trimmed) )
37183746}
37193747
3748+ fn normalized_path ( path : & Path ) -> Result < PathBuf > {
3749+ let absolute = if path. is_absolute ( ) {
3750+ path. to_path_buf ( )
3751+ } else {
3752+ env:: current_dir ( )
3753+ . context ( "resolving current directory for baseline path comparison" ) ?
3754+ . join ( path)
3755+ } ;
3756+ Ok ( fs:: canonicalize ( & absolute) . unwrap_or ( absolute) )
3757+ }
3758+
3759+ fn paths_point_to_same_file ( lhs : & Path , rhs : & Path ) -> Result < bool > {
3760+ Ok ( normalized_path ( lhs) ? == normalized_path ( rhs) ?)
3761+ }
3762+
3763+ fn snapshot_baseline_for_compare ( path : & Path ) -> Result < PathBuf > {
3764+ let stamp = SystemTime :: now ( )
3765+ . duration_since ( UNIX_EPOCH )
3766+ . unwrap_or_else ( |_| Duration :: from_secs ( 0 ) )
3767+ . as_nanos ( ) ;
3768+ let snapshot_path = env:: temp_dir ( ) . join ( format ! ( "mobench-baseline-{stamp}.json" ) ) ;
3769+ fs:: copy ( path, & snapshot_path) . with_context ( || {
3770+ format ! (
3771+ "copying baseline snapshot from {} to {}" ,
3772+ path. display( ) ,
3773+ snapshot_path. display( )
3774+ )
3775+ } ) ?;
3776+ Ok ( snapshot_path)
3777+ }
3778+
37203779fn resolve_artifact_baseline ( reference : & str ) -> Result < PathBuf > {
37213780 if reference. is_empty ( ) {
37223781 bail ! ( "config_error: baseline artifact reference is empty" ) ;
@@ -6692,6 +6751,7 @@ fn check_xcodegen() -> PrereqCheck {
66926751mod tests {
66936752 use super :: * ;
66946753 use jsonschema:: JSONSchema ;
6754+ use tempfile:: TempDir ;
66956755
66966756 // Register a lightweight benchmark for tests so the inventory contains at least one entry.
66976757 #[ mobench_sdk:: benchmark]
@@ -6724,6 +6784,84 @@ mod tests {
67246784 assert ! ( spec. ios_xcuitest. is_none( ) ) ;
67256785 }
67266786
6787+ #[ test]
6788+ fn resolve_run_spec_prefers_cli_device_matrix_with_config ( ) {
6789+ let temp_dir = TempDir :: new ( ) . expect ( "temp dir" ) ;
6790+ let config_matrix_path = temp_dir. path ( ) . join ( "config-matrix.yml" ) ;
6791+ let cli_matrix_path = temp_dir. path ( ) . join ( "cli-matrix.yml" ) ;
6792+ let config_path = temp_dir. path ( ) . join ( "bench-config.toml" ) ;
6793+
6794+ write_file (
6795+ & config_matrix_path,
6796+ br#"devices:
6797+ - name: Config Device
6798+ os: android
6799+ os_version: "14"
6800+ "# ,
6801+ )
6802+ . expect ( "write config matrix" ) ;
6803+ write_file (
6804+ & cli_matrix_path,
6805+ br#"devices:
6806+ - name: CLI Device
6807+ os: android
6808+ os_version: "14"
6809+ "# ,
6810+ )
6811+ . expect ( "write cli matrix" ) ;
6812+
6813+ let config_toml = format ! (
6814+ r#"target = "android"
6815+ function = "sample_fns::fibonacci"
6816+ iterations = 10
6817+ warmup = 2
6818+ device_matrix = "{}"
6819+
6820+ [browserstack]
6821+ app_automate_username = "user"
6822+ app_automate_access_key = "key"
6823+ project = "proj"
6824+ "# ,
6825+ config_matrix_path. display( )
6826+ ) ;
6827+ write_file ( & config_path, config_toml. as_bytes ( ) ) . expect ( "write config" ) ;
6828+
6829+ let spec = resolve_run_spec (
6830+ MobileTarget :: Android ,
6831+ "ignored::value" . into ( ) ,
6832+ 1 ,
6833+ 0 ,
6834+ Vec :: new ( ) ,
6835+ Some ( config_path. as_path ( ) ) ,
6836+ Some ( cli_matrix_path. as_path ( ) ) ,
6837+ Vec :: new ( ) ,
6838+ None ,
6839+ None ,
6840+ false ,
6841+ false ,
6842+ )
6843+ . expect ( "resolve spec" ) ;
6844+
6845+ assert_eq ! ( spec. devices, vec![ "CLI Device" . to_string( ) ] ) ;
6846+ }
6847+
6848+ #[ test]
6849+ fn snapshot_baseline_creates_distinct_copy ( ) {
6850+ let temp_dir = TempDir :: new ( ) . expect ( "temp dir" ) ;
6851+ let baseline = temp_dir. path ( ) . join ( "baseline.json" ) ;
6852+ write_file ( & baseline, br#"{"ok":true}"# ) . expect ( "write baseline" ) ;
6853+
6854+ assert ! ( paths_point_to_same_file( & baseline, & baseline) . expect( "compare path" ) ) ;
6855+
6856+ let snapshot = snapshot_baseline_for_compare ( & baseline) . expect ( "snapshot baseline" ) ;
6857+ assert_ne ! ( snapshot, baseline) ;
6858+ let original_contents = fs:: read_to_string ( & baseline) . expect ( "read baseline" ) ;
6859+ let snapshot_contents = fs:: read_to_string ( & snapshot) . expect ( "read snapshot" ) ;
6860+ assert_eq ! ( snapshot_contents, original_contents) ;
6861+
6862+ fs:: remove_file ( snapshot) . expect ( "remove snapshot" ) ;
6863+ }
6864+
67276865 #[ test]
67286866 fn local_smoke_produces_samples ( ) {
67296867 let spec = RunSpec {
0 commit comments