1use anyhow::Result;
2use config::{Config, ConfigError, File, FileFormat};
3use std::{
4 collections::HashMap,
5 env,
6 fs::File as StdFile,
7 io::{Read, Write},
8 path::{Path, PathBuf},
9};
10use toml_edit::{value, DocumentMut, Item, Table};
11
12use crate::defaults;
13use crate::git::git_interop::{get_head_revision, get_repository_root};
14
15use git_perf_cli_types::DispersionMethod;
17
18pub trait ConfigParentFallbackExt {
23 fn get_with_parent_fallback(&self, parent: &str, name: &str, key: &str) -> Option<String>;
29}
30
31impl ConfigParentFallbackExt for Config {
32 fn get_with_parent_fallback(&self, parent: &str, name: &str, key: &str) -> Option<String> {
33 if let Ok(mut parent_table) = self.get_table(parent) {
38 if let Some(name_value) = parent_table.remove(name) {
40 if let Ok(mut name_table) = name_value.into_table() {
41 if let Some(key_value) = name_table.remove(key) {
42 if let Ok(s) = key_value.into_string() {
43 return Some(s);
44 }
45 }
46 }
47 }
48
49 if let Some(key_value) = parent_table.remove(key) {
51 if let Ok(s) = key_value.into_string() {
52 return Some(s);
53 }
54 }
55 }
56
57 None
58 }
59}
60
61fn get_main_config_path() -> Result<PathBuf> {
63 let repo_root = get_repository_root().map_err(|e| {
65 anyhow::anyhow!(
66 "Failed to determine repository root - must be run from within a git repository: {}",
67 e
68 )
69 })?;
70
71 if repo_root.is_empty() {
72 return Err(anyhow::anyhow!(
73 "Repository root is empty - must be run from within a git repository"
74 ));
75 }
76
77 Ok(PathBuf::from(repo_root).join(".gitperfconfig"))
78}
79
80pub fn write_config(conf: &str) -> Result<()> {
82 let path = get_main_config_path()?;
83 let mut f = StdFile::create(path)?;
84 f.write_all(conf.as_bytes())?;
85 Ok(())
86}
87
88pub fn read_hierarchical_config() -> Result<Config, ConfigError> {
90 let mut builder = Config::builder();
91
92 if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
94 let system_config_path = Path::new(&xdg_config_home)
95 .join("git-perf")
96 .join("config.toml");
97 builder = builder.add_source(
98 File::from(system_config_path)
99 .format(FileFormat::Toml)
100 .required(false),
101 );
102 } else if let Some(home) = dirs_next::home_dir() {
103 let system_config_path = home.join(".config").join("git-perf").join("config.toml");
104 builder = builder.add_source(
105 File::from(system_config_path)
106 .format(FileFormat::Toml)
107 .required(false),
108 );
109 }
110
111 if let Some(local_path) = find_config_path() {
113 builder = builder.add_source(
114 File::from(local_path)
115 .format(FileFormat::Toml)
116 .required(false),
117 );
118 }
119
120 builder.build()
121}
122
123fn find_config_path() -> Option<PathBuf> {
124 let path = get_main_config_path().ok()?;
126 if path.is_file() {
127 Some(path)
128 } else {
129 None
130 }
131}
132
133fn read_config_from_file<P: AsRef<Path>>(file: P) -> Result<String> {
134 let mut conf_str = String::new();
135 StdFile::open(file)?.read_to_string(&mut conf_str)?;
136 Ok(conf_str)
137}
138
139fn read_raw_gitperfconfig() -> Option<String> {
140 let path = find_config_path()?;
141 read_config_from_file(path).ok()
142}
143
144fn read_gitperfconfig_document() -> Option<DocumentMut> {
145 read_raw_gitperfconfig()?.parse::<DocumentMut>().ok()
146}
147
148fn parse_environment_from_doc(doc: &DocumentMut) -> HashMap<String, Vec<String>> {
149 let Some(table) = doc.get("environment").and_then(|item| item.as_table()) else {
150 return HashMap::new();
151 };
152 let mut result = HashMap::new();
153 for (key, item) in table.iter() {
154 if let Some(s) = item.as_str() {
155 result.insert(key.to_string(), vec![s.to_string()]);
156 } else if let Some(arr) = item.as_array() {
157 let vars: Vec<String> = arr
158 .iter()
159 .filter_map(|v| v.as_str())
160 .map(String::from)
161 .collect();
162 if !vars.is_empty() {
163 result.insert(key.to_string(), vars);
164 }
165 } else {
166 log::warn!(
167 "Ignoring unsupported value type for [environment] key '{}'",
168 key
169 );
170 }
171 }
172 result
173}
174
175fn parse_defaults_from_doc(doc: &DocumentMut) -> HashMap<String, String> {
176 let Some(table) = doc.get("defaults").and_then(|item| item.as_table()) else {
177 return HashMap::new();
178 };
179 let mut result = HashMap::new();
180 for (key, item) in table.iter() {
181 if let Some(s) = item.as_str() {
182 result.insert(key.to_string(), s.to_string());
183 } else {
184 log::warn!("Ignoring non-string value for [defaults] key '{}'", key);
185 }
186 }
187 result
188}
189
190#[must_use]
196pub fn read_environment_config() -> HashMap<String, Vec<String>> {
197 read_gitperfconfig_document()
198 .map(|doc| parse_environment_from_doc(&doc))
199 .unwrap_or_default()
200}
201
202#[must_use]
208pub fn read_defaults_config() -> HashMap<String, String> {
209 read_gitperfconfig_document()
210 .map(|doc| parse_defaults_from_doc(&doc))
211 .unwrap_or_default()
212}
213
214fn apply_env_source(result: &mut HashMap<String, String>, key: String, var_names: Vec<String>) {
215 for var_name in &var_names {
216 if let Ok(val) = std::env::var(var_name) {
217 if !val.is_empty() {
218 result.insert(key, val);
219 break;
220 }
221 }
222 }
223}
224
225#[must_use]
233pub fn resolve_key_values(
234 cli_key_values: &[(String, String)],
235 skip_env: bool,
236) -> Vec<(String, String)> {
237 let mut result: HashMap<String, String> = HashMap::new();
238
239 if let Some(doc) = read_gitperfconfig_document() {
241 for (key, value) in parse_defaults_from_doc(&doc) {
243 result.insert(key, value);
244 }
245
246 if !skip_env {
248 for (key, source) in parse_environment_from_doc(&doc) {
249 apply_env_source(&mut result, key, source);
250 }
251 }
252 }
253
254 for (key, value) in cli_key_values {
256 result.insert(key.clone(), value.clone());
257 }
258
259 result.into_iter().collect()
260}
261
262#[must_use]
263pub fn determine_epoch_from_config(measurement: &str) -> Option<u32> {
264 let config = read_hierarchical_config()
265 .map_err(|e| {
266 log::debug!("Could not read hierarchical config: {}", e);
268 })
269 .ok()?;
270
271 config
273 .get_with_parent_fallback("measurement", measurement, "epoch")
274 .and_then(|s| u32::from_str_radix(&s, 16).ok())
275}
276
277pub fn bump_epoch_in_conf(measurement: &str, conf_str: &mut String) -> Result<()> {
278 let mut conf = conf_str
279 .parse::<DocumentMut>()
280 .map_err(|e| anyhow::anyhow!("Failed to parse config: {}", e))?;
281
282 let head_revision = get_head_revision()?;
283
284 if !conf.contains_key("measurement") {
286 conf["measurement"] = Item::Table(Table::new());
287 }
288 if !conf["measurement"]
289 .as_table()
290 .unwrap()
291 .contains_key(measurement)
292 {
293 conf["measurement"][measurement] = Item::Table(Table::new());
294 }
295
296 conf["measurement"][measurement]["epoch"] = value(&head_revision[0..8]);
297 *conf_str = conf.to_string();
298
299 Ok(())
300}
301
302pub fn bump_epoch(measurement: &str) -> Result<()> {
303 let config_path = get_main_config_path()?;
305 let mut conf_str = read_config_from_file(&config_path).unwrap_or_default();
306
307 bump_epoch_in_conf(measurement, &mut conf_str)?;
308 write_config(&conf_str)?;
309 Ok(())
310}
311
312#[must_use]
314pub fn backoff_max_elapsed_seconds() -> u64 {
315 match read_hierarchical_config() {
316 Ok(config) => {
317 if let Ok(seconds) = config.get_int("backoff.max_elapsed_seconds") {
318 seconds as u64
319 } else {
320 defaults::DEFAULT_BACKOFF_MAX_ELAPSED_SECONDS
321 }
322 }
323 Err(_) => defaults::DEFAULT_BACKOFF_MAX_ELAPSED_SECONDS,
324 }
325}
326
327#[must_use]
329pub fn audit_min_relative_deviation(measurement: &str) -> Option<f64> {
330 let config = read_hierarchical_config().ok()?;
331
332 if let Some(s) =
333 config.get_with_parent_fallback("measurement", measurement, "min_relative_deviation")
334 {
335 if let Ok(v) = s.parse::<f64>() {
336 return Some(v);
337 }
338 }
339
340 None
341}
342
343#[must_use]
347pub fn audit_max_cov(measurement: &str) -> Option<f64> {
348 let config = read_hierarchical_config().ok()?;
349
350 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "max_cov") {
351 if let Ok(v) = s.parse::<f64>() {
352 return Some(v);
353 }
354 }
355
356 None
357}
358
359#[must_use]
361pub fn audit_min_absolute_deviation(measurement: &str) -> Option<f64> {
362 let config = read_hierarchical_config().ok()?;
363
364 if let Some(s) =
365 config.get_with_parent_fallback("measurement", measurement, "min_absolute_deviation")
366 {
367 if let Ok(v) = s.parse::<f64>() {
368 return Some(v);
369 }
370 }
371
372 None
373}
374
375#[must_use]
377pub fn audit_dispersion_method(measurement: &str) -> DispersionMethod {
378 let Some(config) = read_hierarchical_config().ok() else {
379 return DispersionMethod::StandardDeviation;
380 };
381
382 if let Some(s) =
383 config.get_with_parent_fallback("measurement", measurement, "dispersion_method")
384 {
385 if let Ok(method) = s.parse::<DispersionMethod>() {
386 return method;
387 }
388 }
389
390 DispersionMethod::StandardDeviation
391}
392
393#[must_use]
395pub fn audit_min_measurements(measurement: &str) -> Option<u16> {
396 let config = read_hierarchical_config().ok()?;
397
398 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "min_measurements")
399 {
400 if let Ok(v) = s.parse::<u16>() {
401 return Some(v);
402 }
403 }
404
405 None
406}
407
408#[must_use]
410pub fn audit_aggregate_by(measurement: &str) -> Option<git_perf_cli_types::ReductionFunc> {
411 let config = read_hierarchical_config().ok()?;
412
413 let s = config.get_with_parent_fallback("measurement", measurement, "aggregate_by")?;
414
415 match s.to_lowercase().as_str() {
417 "min" => Some(git_perf_cli_types::ReductionFunc::Min),
418 "max" => Some(git_perf_cli_types::ReductionFunc::Max),
419 "median" => Some(git_perf_cli_types::ReductionFunc::Median),
420 "mean" => Some(git_perf_cli_types::ReductionFunc::Mean),
421 _ => None,
422 }
423}
424
425#[must_use]
427pub fn audit_sigma(measurement: &str) -> Option<f64> {
428 let config = read_hierarchical_config().ok()?;
429
430 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "sigma") {
431 if let Ok(v) = s.parse::<f64>() {
432 return Some(v);
433 }
434 }
435
436 None
437}
438
439#[must_use]
441pub fn measurement_unit(measurement: &str) -> Option<String> {
442 let config = read_hierarchical_config().ok()?;
443 config.get_with_parent_fallback("measurement", measurement, "unit")
444}
445
446#[must_use]
448pub fn report_template_path() -> Option<PathBuf> {
449 let config = read_hierarchical_config().ok()?;
450 let path_str = config.get_string("report.template_path").ok()?;
451 Some(PathBuf::from(path_str))
452}
453
454#[must_use]
456pub fn report_custom_css_path() -> Option<PathBuf> {
457 let config = read_hierarchical_config().ok()?;
458 let path_str = config.get_string("report.custom_css_path").ok()?;
459 Some(PathBuf::from(path_str))
460}
461
462#[must_use]
464pub fn report_title() -> Option<String> {
465 let config = read_hierarchical_config().ok()?;
466 config.get_string("report.title").ok()
467}
468
469#[must_use]
478pub fn change_point_config(measurement: &str) -> crate::change_point::ChangePointConfig {
479 let mut config = crate::change_point::ChangePointConfig::default();
480
481 let Ok(file_config) = read_hierarchical_config() else {
482 return config;
483 };
484
485 if let Some(enabled_str) =
487 file_config.get_with_parent_fallback("change_point", measurement, "enabled")
488 {
489 if let Ok(enabled) = enabled_str.parse::<bool>() {
490 config.enabled = enabled;
491 }
492 }
493
494 if let Some(s) =
496 file_config.get_with_parent_fallback("change_point", measurement, "min_data_points")
497 {
498 if let Ok(v) = s.parse::<usize>() {
499 config.min_data_points = v;
500 }
501 }
502
503 if let Some(s) =
505 file_config.get_with_parent_fallback("change_point", measurement, "min_magnitude_pct")
506 {
507 if let Ok(v) = s.parse::<f64>() {
508 config.min_magnitude_pct = v;
509 }
510 }
511
512 if let Some(s) =
514 file_config.get_with_parent_fallback("change_point", measurement, "confidence_threshold")
515 {
516 if let Ok(v) = s.parse::<f64>() {
517 config.confidence_threshold = v;
518 }
519 }
520
521 if let Some(s) = file_config.get_with_parent_fallback("change_point", measurement, "penalty") {
523 if let Ok(v) = s.parse::<f64>() {
524 config.penalty = v;
525 }
526 }
527
528 config
529}
530
531#[cfg(test)]
532mod test {
533 use super::*;
534 use crate::test_helpers::{
535 hermetic_git_env, init_repo, init_repo_with_file, with_isolated_home,
536 };
537 use std::fs;
538 use tempfile::TempDir;
539
540 fn create_home_config_dir(home_dir: &Path) -> PathBuf {
542 let config_dir = home_dir.join(".config").join("git-perf");
543 fs::create_dir_all(&config_dir).unwrap();
544 config_dir.join("config.toml")
545 }
546
547 #[test]
548 fn test_read_epochs() {
549 with_isolated_home(|temp_dir| {
550 env::set_current_dir(temp_dir).unwrap();
552 init_repo(temp_dir);
553
554 let workspace_config_path = temp_dir.join(".gitperfconfig");
556 let configfile = r#"[measurement]
557# General performance regression
558epoch="12344555"
559
560[measurement."something"]
561#My comment
562epoch="34567898"
563
564[measurement."somethingelse"]
565epoch="a3dead"
566"#;
567 fs::write(&workspace_config_path, configfile).unwrap();
568
569 let epoch = determine_epoch_from_config("something");
570 assert_eq!(epoch, Some(0x34567898));
571
572 let epoch = determine_epoch_from_config("somethingelse");
573 assert_eq!(epoch, Some(0xa3dead));
574
575 let epoch = determine_epoch_from_config("unspecified");
576 assert_eq!(epoch, Some(0x12344555));
577 });
578 }
579
580 #[test]
581 fn test_bump_epochs() {
582 with_isolated_home(|temp_dir| {
583 env::set_current_dir(temp_dir).unwrap();
585
586 hermetic_git_env();
588
589 init_repo_with_file(temp_dir);
591
592 let configfile = r#"[measurement."something"]
593#My comment
594epoch = "34567898"
595"#;
596
597 let mut actual = String::from(configfile);
598 bump_epoch_in_conf("something", &mut actual).expect("Failed to bump epoch");
599
600 let expected = format!(
601 r#"[measurement."something"]
602#My comment
603epoch = "{}"
604"#,
605 &get_head_revision().expect("get_head_revision failed")[0..8],
606 );
607
608 assert_eq!(actual, expected);
609 });
610 }
611
612 #[test]
613 fn test_bump_new_epoch_and_read_it() {
614 with_isolated_home(|temp_dir| {
615 env::set_current_dir(temp_dir).unwrap();
617
618 hermetic_git_env();
620
621 init_repo_with_file(temp_dir);
623
624 let mut conf = String::new();
625 bump_epoch_in_conf("mymeasurement", &mut conf).expect("Failed to bump epoch");
626
627 let config_path = temp_dir.join(".gitperfconfig");
629 fs::write(&config_path, &conf).unwrap();
630
631 let epoch = determine_epoch_from_config("mymeasurement");
632 assert!(epoch.is_some());
633 });
634 }
635
636 #[test]
637 fn test_backoff_max_elapsed_seconds() {
638 with_isolated_home(|temp_dir| {
639 env::set_current_dir(temp_dir).unwrap();
641 init_repo(temp_dir);
642
643 let workspace_config_path = temp_dir.join(".gitperfconfig");
645 let local_config = "[backoff]\nmax_elapsed_seconds = 42\n";
646 fs::write(&workspace_config_path, local_config).unwrap();
647
648 assert_eq!(super::backoff_max_elapsed_seconds(), 42);
650
651 fs::remove_file(&workspace_config_path).unwrap();
653 assert_eq!(super::backoff_max_elapsed_seconds(), 60);
654 });
655 }
656
657 #[test]
658 fn test_audit_min_relative_deviation() {
659 with_isolated_home(|temp_dir| {
660 env::set_current_dir(temp_dir).unwrap();
662 init_repo(temp_dir);
663
664 let workspace_config_path = temp_dir.join(".gitperfconfig");
666 let local_config = r#"
667[measurement]
668min_relative_deviation = 5.0
669
670[measurement."build_time"]
671min_relative_deviation = 10.0
672
673[measurement."memory_usage"]
674min_relative_deviation = 2.5
675"#;
676 fs::write(&workspace_config_path, local_config).unwrap();
677
678 assert_eq!(
680 super::audit_min_relative_deviation("build_time"),
681 Some(10.0)
682 );
683 assert_eq!(
684 super::audit_min_relative_deviation("memory_usage"),
685 Some(2.5)
686 );
687 assert_eq!(
688 super::audit_min_relative_deviation("other_measurement"),
689 Some(5.0) );
691
692 let global_config = r#"
694[measurement]
695min_relative_deviation = 5.0
696"#;
697 fs::write(&workspace_config_path, global_config).unwrap();
698 assert_eq!(
699 super::audit_min_relative_deviation("any_measurement"),
700 Some(5.0)
701 );
702
703 let precedence_config = r#"
705[measurement]
706min_relative_deviation = 5.0
707
708[measurement."build_time"]
709min_relative_deviation = 10.0
710"#;
711 fs::write(&workspace_config_path, precedence_config).unwrap();
712 assert_eq!(
713 super::audit_min_relative_deviation("build_time"),
714 Some(10.0)
715 );
716 assert_eq!(
717 super::audit_min_relative_deviation("other_measurement"),
718 Some(5.0)
719 );
720
721 fs::remove_file(&workspace_config_path).unwrap();
723 assert_eq!(super::audit_min_relative_deviation("any_measurement"), None);
724 });
725 }
726
727 #[test]
728 fn test_audit_max_cov() {
729 with_isolated_home(|temp_dir| {
730 env::set_current_dir(temp_dir).unwrap();
731 init_repo(temp_dir);
732
733 let workspace_config_path = temp_dir.join(".gitperfconfig");
734 let local_config = r#"
735[measurement]
736max_cov = 30.0
737
738[measurement."build_time"]
739max_cov = 50.0
740
741[measurement."memory_usage"]
742max_cov = 20.0
743"#;
744 fs::write(&workspace_config_path, local_config).unwrap();
745
746 assert_eq!(super::audit_max_cov("build_time"), Some(50.0));
747 assert_eq!(super::audit_max_cov("memory_usage"), Some(20.0));
748 assert_eq!(super::audit_max_cov("other_measurement"), Some(30.0));
749
750 let global_config = r#"
751[measurement]
752max_cov = 30.0
753"#;
754 fs::write(&workspace_config_path, global_config).unwrap();
755 assert_eq!(super::audit_max_cov("any_measurement"), Some(30.0));
756
757 fs::remove_file(&workspace_config_path).unwrap();
758 assert_eq!(super::audit_max_cov("any_measurement"), None);
759 });
760 }
761
762 #[test]
763 fn test_audit_min_absolute_deviation() {
764 with_isolated_home(|temp_dir| {
765 env::set_current_dir(temp_dir).unwrap();
767 init_repo(temp_dir);
768
769 let workspace_config_path = temp_dir.join(".gitperfconfig");
771 let local_config = r#"
772[measurement]
773min_absolute_deviation = 5.0
774
775[measurement."build_time"]
776min_absolute_deviation = 10.0
777
778[measurement."memory_usage"]
779min_absolute_deviation = 2.5
780"#;
781 fs::write(&workspace_config_path, local_config).unwrap();
782
783 assert_eq!(
785 super::audit_min_absolute_deviation("build_time"),
786 Some(10.0)
787 );
788 assert_eq!(
789 super::audit_min_absolute_deviation("memory_usage"),
790 Some(2.5)
791 );
792 assert_eq!(
793 super::audit_min_absolute_deviation("other_measurement"),
794 Some(5.0) );
796
797 let global_config = r#"
799[measurement]
800min_absolute_deviation = 5.0
801"#;
802 fs::write(&workspace_config_path, global_config).unwrap();
803 assert_eq!(
804 super::audit_min_absolute_deviation("any_measurement"),
805 Some(5.0)
806 );
807
808 let precedence_config = r#"
810[measurement]
811min_absolute_deviation = 5.0
812
813[measurement."build_time"]
814min_absolute_deviation = 10.0
815"#;
816 fs::write(&workspace_config_path, precedence_config).unwrap();
817 assert_eq!(
818 super::audit_min_absolute_deviation("build_time"),
819 Some(10.0)
820 );
821 assert_eq!(
822 super::audit_min_absolute_deviation("other_measurement"),
823 Some(5.0)
824 );
825
826 fs::remove_file(&workspace_config_path).unwrap();
828 assert_eq!(super::audit_min_absolute_deviation("any_measurement"), None);
829 });
830 }
831
832 #[test]
833 fn test_audit_dispersion_method() {
834 with_isolated_home(|temp_dir| {
835 env::set_current_dir(temp_dir).unwrap();
837 init_repo(temp_dir);
838
839 let workspace_config_path = temp_dir.join(".gitperfconfig");
841 let local_config = r#"
842[measurement]
843dispersion_method = "stddev"
844
845[measurement."build_time"]
846dispersion_method = "mad"
847
848[measurement."memory_usage"]
849dispersion_method = "stddev"
850"#;
851 fs::write(&workspace_config_path, local_config).unwrap();
852
853 assert_eq!(
855 super::audit_dispersion_method("build_time"),
856 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
857 );
858 assert_eq!(
859 super::audit_dispersion_method("memory_usage"),
860 git_perf_cli_types::DispersionMethod::StandardDeviation
861 );
862 assert_eq!(
863 super::audit_dispersion_method("other_measurement"),
864 git_perf_cli_types::DispersionMethod::StandardDeviation
865 );
866
867 let global_config = r#"
869[measurement]
870dispersion_method = "mad"
871"#;
872 fs::write(&workspace_config_path, global_config).unwrap();
873 assert_eq!(
874 super::audit_dispersion_method("any_measurement"),
875 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
876 );
877
878 let precedence_config = r#"
880[measurement]
881dispersion_method = "mad"
882
883[measurement."build_time"]
884dispersion_method = "stddev"
885"#;
886 fs::write(&workspace_config_path, precedence_config).unwrap();
887 assert_eq!(
888 super::audit_dispersion_method("build_time"),
889 git_perf_cli_types::DispersionMethod::StandardDeviation
890 );
891 assert_eq!(
892 super::audit_dispersion_method("other_measurement"),
893 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
894 );
895
896 fs::remove_file(&workspace_config_path).unwrap();
898 assert_eq!(
899 super::audit_dispersion_method("any_measurement"),
900 git_perf_cli_types::DispersionMethod::StandardDeviation
901 );
902 });
903 }
904
905 #[test]
906 fn test_bump_epoch_in_conf_creates_proper_tables() {
907 with_isolated_home(|temp_dir| {
910 env::set_current_dir(temp_dir).unwrap();
911
912 hermetic_git_env();
914
915 init_repo_with_file(temp_dir);
916
917 let mut empty_config = String::new();
919
920 bump_epoch_in_conf("mymeasurement", &mut empty_config).unwrap();
922
923 assert!(empty_config.contains("[measurement]"));
925 assert!(empty_config.contains("[measurement.mymeasurement]"));
926 assert!(empty_config.contains("epoch ="));
927 assert!(!empty_config.contains("measurement = {"));
929 assert!(!empty_config.contains("mymeasurement = {"));
930
931 let mut existing_config = r#"[measurement]
933existing_setting = "value"
934
935[measurement."other"]
936epoch = "oldvalue"
937"#
938 .to_string();
939
940 bump_epoch_in_conf("newmeasurement", &mut existing_config).unwrap();
941
942 assert!(existing_config.contains("[measurement.newmeasurement]"));
944 assert!(existing_config.contains("existing_setting = \"value\""));
945 assert!(existing_config.contains("[measurement.\"other\"]"));
946 assert!(!existing_config.contains("newmeasurement = {"));
947 });
948 }
949
950 #[test]
951 fn test_find_config_path_in_git_root() {
952 with_isolated_home(|temp_dir| {
953 env::set_current_dir(temp_dir).unwrap();
955
956 init_repo(temp_dir);
958
959 let config_path = temp_dir.join(".gitperfconfig");
961 fs::write(
962 &config_path,
963 "[measurement.\"test\"]\nepoch = \"12345678\"\n",
964 )
965 .unwrap();
966
967 let found_path = find_config_path();
969 assert!(found_path.is_some());
970 assert_eq!(
972 found_path.unwrap().canonicalize().unwrap(),
973 config_path.canonicalize().unwrap()
974 );
975 });
976 }
977
978 #[test]
979 fn test_find_config_path_not_found() {
980 with_isolated_home(|temp_dir| {
981 env::set_current_dir(temp_dir).unwrap();
983
984 init_repo(temp_dir);
986
987 let found_path = find_config_path();
989 assert!(found_path.is_none());
990 });
991 }
992
993 #[test]
994 fn test_hierarchical_config_workspace_overrides_home() {
995 with_isolated_home(|temp_dir| {
996 env::set_current_dir(temp_dir).unwrap();
998
999 init_repo(temp_dir);
1001
1002 let home_config_path = create_home_config_dir(temp_dir);
1004 fs::write(
1005 &home_config_path,
1006 r#"
1007[measurement."test"]
1008backoff_max_elapsed_seconds = 30
1009audit_min_relative_deviation = 1.0
1010"#,
1011 )
1012 .unwrap();
1013
1014 let workspace_config_path = temp_dir.join(".gitperfconfig");
1016 fs::write(
1017 &workspace_config_path,
1018 r#"
1019[measurement."test"]
1020backoff_max_elapsed_seconds = 60
1021"#,
1022 )
1023 .unwrap();
1024
1025 env::set_var("HOME", temp_dir);
1027 env::remove_var("XDG_CONFIG_HOME");
1028
1029 let config = read_hierarchical_config().unwrap();
1031
1032 let backoff: i32 = config
1034 .get("measurement.test.backoff_max_elapsed_seconds")
1035 .unwrap();
1036 assert_eq!(backoff, 60);
1037
1038 let deviation: f64 = config
1040 .get("measurement.test.audit_min_relative_deviation")
1041 .unwrap();
1042 assert_eq!(deviation, 1.0);
1043 });
1044 }
1045
1046 #[test]
1047 fn test_determine_epoch_from_config_with_missing_file() {
1048 let temp_dir = TempDir::new().unwrap();
1050 fs::create_dir_all(temp_dir.path()).unwrap();
1051 env::set_current_dir(temp_dir.path()).unwrap();
1052
1053 let epoch = determine_epoch_from_config("test_measurement");
1054 assert!(epoch.is_none());
1055 }
1056
1057 #[test]
1058 fn test_determine_epoch_from_config_with_invalid_toml() {
1059 let temp_dir = TempDir::new().unwrap();
1060 let config_path = temp_dir.path().join(".gitperfconfig");
1061 fs::write(&config_path, "invalid toml content").unwrap();
1062
1063 fs::create_dir_all(temp_dir.path()).unwrap();
1064 env::set_current_dir(temp_dir.path()).unwrap();
1065
1066 let epoch = determine_epoch_from_config("test_measurement");
1067 assert!(epoch.is_none());
1068 }
1069
1070 #[test]
1071 fn test_write_config_creates_file() {
1072 with_isolated_home(|temp_dir| {
1073 env::set_current_dir(temp_dir).unwrap();
1075 init_repo(temp_dir);
1076
1077 let subdir = temp_dir.join("a").join("b").join("c");
1079 fs::create_dir_all(&subdir).unwrap();
1080 env::set_current_dir(&subdir).unwrap();
1081
1082 let config_content = "[measurement.\"test\"]\nepoch = \"12345678\"\n";
1083 write_config(config_content).unwrap();
1084
1085 let repo_config_path = temp_dir.join(".gitperfconfig");
1087 let subdir_config_path = subdir.join(".gitperfconfig");
1088
1089 assert!(repo_config_path.is_file());
1090 assert!(!subdir_config_path.is_file());
1091
1092 let content = fs::read_to_string(&repo_config_path).unwrap();
1093 assert_eq!(content, config_content);
1094 });
1095 }
1096
1097 #[test]
1098 fn test_hierarchical_config_system_override() {
1099 with_isolated_home(|temp_dir| {
1100 let system_config_path = create_home_config_dir(temp_dir);
1102 let system_config = r#"
1103[measurement]
1104min_relative_deviation = 5.0
1105dispersion_method = "mad"
1106
1107[backoff]
1108max_elapsed_seconds = 120
1109"#;
1110 fs::write(&system_config_path, system_config).unwrap();
1111
1112 env::set_current_dir(temp_dir).unwrap();
1114 init_repo(temp_dir);
1115
1116 let workspace_config_path = temp_dir.join(".gitperfconfig");
1118 let local_config = r#"
1119[measurement]
1120min_relative_deviation = 10.0
1121
1122[measurement."build_time"]
1123min_relative_deviation = 15.0
1124dispersion_method = "stddev"
1125"#;
1126 fs::write(&workspace_config_path, local_config).unwrap();
1127
1128 let config = read_hierarchical_config().unwrap();
1130
1131 use super::ConfigParentFallbackExt;
1133 assert_eq!(
1134 config
1135 .get_with_parent_fallback(
1136 "measurement",
1137 "any_measurement",
1138 "min_relative_deviation"
1139 )
1140 .unwrap()
1141 .parse::<f64>()
1142 .unwrap(),
1143 10.0
1144 );
1145 assert_eq!(
1146 config
1147 .get_with_parent_fallback("measurement", "any_measurement", "dispersion_method")
1148 .unwrap(),
1149 "mad"
1150 ); assert_eq!(
1154 config
1155 .get_float("measurement.build_time.min_relative_deviation")
1156 .unwrap(),
1157 15.0
1158 );
1159 assert_eq!(
1160 config
1161 .get_string("measurement.build_time.dispersion_method")
1162 .unwrap(),
1163 "stddev"
1164 );
1165
1166 assert_eq!(config.get_int("backoff.max_elapsed_seconds").unwrap(), 120);
1168
1169 assert_eq!(audit_min_relative_deviation("build_time"), Some(15.0));
1171 assert_eq!(
1172 audit_min_relative_deviation("other_measurement"),
1173 Some(10.0)
1174 );
1175 assert_eq!(
1176 audit_dispersion_method("build_time"),
1177 git_perf_cli_types::DispersionMethod::StandardDeviation
1178 );
1179 assert_eq!(
1180 audit_dispersion_method("other_measurement"),
1181 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
1182 );
1183 assert_eq!(backoff_max_elapsed_seconds(), 120);
1184 });
1185 }
1186
1187 #[test]
1188 fn test_read_config_from_file_missing_file() {
1189 let temp_dir = TempDir::new().unwrap();
1190 let nonexistent_file = temp_dir.path().join("does_not_exist.toml");
1191
1192 let result = read_config_from_file(&nonexistent_file);
1194 assert!(result.is_err());
1195 }
1196
1197 #[test]
1198 fn test_read_config_from_file_valid_content() {
1199 let temp_dir = TempDir::new().unwrap();
1200 let config_file = temp_dir.path().join("test_config.toml");
1201 let expected_content = "[measurement]\nepoch = \"12345678\"\n";
1202
1203 fs::write(&config_file, expected_content).unwrap();
1204
1205 let result = read_config_from_file(&config_file);
1206 assert!(result.is_ok());
1207 let content = result.unwrap();
1208 assert_eq!(content, expected_content);
1209
1210 assert!(!content.is_empty());
1212 }
1213
1214 #[test]
1215 fn test_audit_min_measurements() {
1216 with_isolated_home(|temp_dir| {
1217 env::set_current_dir(temp_dir).unwrap();
1219 init_repo(temp_dir);
1220
1221 let workspace_config_path = temp_dir.join(".gitperfconfig");
1223 let local_config = r#"
1224[measurement]
1225min_measurements = 5
1226
1227[measurement."build_time"]
1228min_measurements = 10
1229
1230[measurement."memory_usage"]
1231min_measurements = 3
1232"#;
1233 fs::write(&workspace_config_path, local_config).unwrap();
1234
1235 assert_eq!(super::audit_min_measurements("build_time"), Some(10));
1237 assert_eq!(super::audit_min_measurements("memory_usage"), Some(3));
1238 assert_eq!(super::audit_min_measurements("other_measurement"), Some(5));
1239
1240 fs::remove_file(&workspace_config_path).unwrap();
1242 assert_eq!(super::audit_min_measurements("any_measurement"), None);
1243 });
1244 }
1245
1246 #[test]
1247 fn test_audit_aggregate_by() {
1248 with_isolated_home(|temp_dir| {
1249 env::set_current_dir(temp_dir).unwrap();
1251 init_repo(temp_dir);
1252
1253 let workspace_config_path = temp_dir.join(".gitperfconfig");
1255 let local_config = r#"
1256[measurement]
1257aggregate_by = "median"
1258
1259[measurement."build_time"]
1260aggregate_by = "max"
1261
1262[measurement."memory_usage"]
1263aggregate_by = "mean"
1264"#;
1265 fs::write(&workspace_config_path, local_config).unwrap();
1266
1267 assert_eq!(
1269 super::audit_aggregate_by("build_time"),
1270 Some(git_perf_cli_types::ReductionFunc::Max)
1271 );
1272 assert_eq!(
1273 super::audit_aggregate_by("memory_usage"),
1274 Some(git_perf_cli_types::ReductionFunc::Mean)
1275 );
1276 assert_eq!(
1277 super::audit_aggregate_by("other_measurement"),
1278 Some(git_perf_cli_types::ReductionFunc::Median)
1279 );
1280
1281 fs::remove_file(&workspace_config_path).unwrap();
1283 assert_eq!(super::audit_aggregate_by("any_measurement"), None);
1284 });
1285 }
1286
1287 #[test]
1288 fn test_audit_sigma() {
1289 with_isolated_home(|temp_dir| {
1290 env::set_current_dir(temp_dir).unwrap();
1292 init_repo(temp_dir);
1293
1294 let workspace_config_path = temp_dir.join(".gitperfconfig");
1296 let local_config = r#"
1297[measurement]
1298sigma = 3.0
1299
1300[measurement."build_time"]
1301sigma = 5.5
1302
1303[measurement."memory_usage"]
1304sigma = 2.0
1305"#;
1306 fs::write(&workspace_config_path, local_config).unwrap();
1307
1308 assert_eq!(super::audit_sigma("build_time"), Some(5.5));
1310 assert_eq!(super::audit_sigma("memory_usage"), Some(2.0));
1311 assert_eq!(super::audit_sigma("other_measurement"), Some(3.0));
1312
1313 fs::remove_file(&workspace_config_path).unwrap();
1315 assert_eq!(super::audit_sigma("any_measurement"), None);
1316 });
1317 }
1318
1319 #[test]
1320 fn test_measurement_unit() {
1321 with_isolated_home(|temp_dir| {
1322 env::set_current_dir(temp_dir).unwrap();
1324 init_repo(temp_dir);
1325
1326 let workspace_config_path = temp_dir.join(".gitperfconfig");
1328 let local_config = r#"
1329[measurement]
1330unit = "ms"
1331
1332[measurement."build_time"]
1333unit = "ms"
1334
1335[measurement."memory_usage"]
1336unit = "bytes"
1337
1338[measurement."throughput"]
1339unit = "requests/sec"
1340"#;
1341 fs::write(&workspace_config_path, local_config).unwrap();
1342
1343 assert_eq!(
1345 super::measurement_unit("build_time"),
1346 Some("ms".to_string())
1347 );
1348 assert_eq!(
1349 super::measurement_unit("memory_usage"),
1350 Some("bytes".to_string())
1351 );
1352 assert_eq!(
1353 super::measurement_unit("throughput"),
1354 Some("requests/sec".to_string())
1355 );
1356
1357 assert_eq!(
1359 super::measurement_unit("other_measurement"),
1360 Some("ms".to_string())
1361 );
1362
1363 fs::remove_file(&workspace_config_path).unwrap();
1365 assert_eq!(super::measurement_unit("any_measurement"), None);
1366 });
1367 }
1368
1369 #[test]
1370 fn test_measurement_unit_precedence() {
1371 with_isolated_home(|temp_dir| {
1372 env::set_current_dir(temp_dir).unwrap();
1374 init_repo(temp_dir);
1375
1376 let workspace_config_path = temp_dir.join(".gitperfconfig");
1378 let precedence_config = r#"
1379[measurement]
1380unit = "ms"
1381
1382[measurement."build_time"]
1383unit = "seconds"
1384"#;
1385 fs::write(&workspace_config_path, precedence_config).unwrap();
1386
1387 assert_eq!(
1389 super::measurement_unit("build_time"),
1390 Some("seconds".to_string())
1391 );
1392
1393 assert_eq!(
1395 super::measurement_unit("other_measurement"),
1396 Some("ms".to_string())
1397 );
1398 });
1399 }
1400
1401 #[test]
1402 fn test_read_environment_config_single_var() {
1403 with_isolated_home(|temp_dir| {
1404 env::set_current_dir(temp_dir).unwrap();
1405 init_repo(temp_dir);
1406 let config_path = temp_dir.join(".gitperfconfig");
1407 fs::write(
1408 &config_path,
1409 "[environment]\ncommit = \"TEST_GITPERF_SHA\"\n",
1410 )
1411 .unwrap();
1412 let cfg = read_environment_config();
1413 assert_eq!(
1414 cfg.get("commit"),
1415 Some(&vec!["TEST_GITPERF_SHA".to_string()])
1416 );
1417 });
1418 }
1419
1420 #[test]
1421 fn test_read_environment_config_multi_var() {
1422 with_isolated_home(|temp_dir| {
1423 env::set_current_dir(temp_dir).unwrap();
1424 init_repo(temp_dir);
1425 let config_path = temp_dir.join(".gitperfconfig");
1426 fs::write(
1427 &config_path,
1428 "[environment]\nrunner_id = [\"GITPERF_R1\", \"GITPERF_R2\"]\n",
1429 )
1430 .unwrap();
1431 let cfg = read_environment_config();
1432 assert_eq!(
1433 cfg.get("runner_id"),
1434 Some(&vec!["GITPERF_R1".to_string(), "GITPERF_R2".to_string()])
1435 );
1436 });
1437 }
1438
1439 #[test]
1440 fn test_read_defaults_config() {
1441 with_isolated_home(|temp_dir| {
1442 env::set_current_dir(temp_dir).unwrap();
1443 init_repo(temp_dir);
1444 let config_path = temp_dir.join(".gitperfconfig");
1445 fs::write(&config_path, "[defaults]\nenvironment = \"local\"\n").unwrap();
1446 let cfg = read_defaults_config();
1447 assert_eq!(cfg.get("environment"), Some(&"local".to_string()));
1448 });
1449 }
1450
1451 #[test]
1452 fn test_read_environment_config_missing_section() {
1453 with_isolated_home(|temp_dir| {
1454 env::set_current_dir(temp_dir).unwrap();
1455 init_repo(temp_dir);
1456 let config_path = temp_dir.join(".gitperfconfig");
1457 fs::write(&config_path, "[measurement]\n").unwrap();
1458 assert!(read_environment_config().is_empty());
1459 });
1460 }
1461
1462 #[test]
1463 fn test_read_defaults_config_missing_section() {
1464 with_isolated_home(|temp_dir| {
1465 env::set_current_dir(temp_dir).unwrap();
1466 init_repo(temp_dir);
1467 let config_path = temp_dir.join(".gitperfconfig");
1468 fs::write(&config_path, "[measurement]\n").unwrap();
1469 assert!(read_defaults_config().is_empty());
1470 });
1471 }
1472
1473 #[test]
1474 fn test_resolve_key_values_cli_wins_over_env() {
1475 with_isolated_home(|temp_dir| {
1476 env::set_current_dir(temp_dir).unwrap();
1477 init_repo(temp_dir);
1478 let config_path = temp_dir.join(".gitperfconfig");
1479 fs::write(
1480 &config_path,
1481 "[environment]\nfoo = \"GITPERF_TEST_CLI_WINS\"\n",
1482 )
1483 .unwrap();
1484 env::set_var("GITPERF_TEST_CLI_WINS", "from_env");
1485 let result = resolve_key_values(&[("foo".to_string(), "from_cli".to_string())], false);
1486 env::remove_var("GITPERF_TEST_CLI_WINS");
1487 assert!(result.contains(&("foo".to_string(), "from_cli".to_string())));
1488 });
1489 }
1490
1491 #[test]
1492 fn test_resolve_key_values_env_wins_over_defaults() {
1493 with_isolated_home(|temp_dir| {
1494 env::set_current_dir(temp_dir).unwrap();
1495 init_repo(temp_dir);
1496 let config_path = temp_dir.join(".gitperfconfig");
1497 fs::write(
1498 &config_path,
1499 "[environment]\nfoo = \"GITPERF_TEST_ENV_WINS\"\n[defaults]\nfoo = \"from_defaults\"\n",
1500 )
1501 .unwrap();
1502 env::set_var("GITPERF_TEST_ENV_WINS", "from_env");
1503 let result = resolve_key_values(&[], false);
1504 env::remove_var("GITPERF_TEST_ENV_WINS");
1505 assert!(result.contains(&("foo".to_string(), "from_env".to_string())));
1506 });
1507 }
1508
1509 #[test]
1510 fn test_resolve_key_values_defaults_when_env_unset() {
1511 with_isolated_home(|temp_dir| {
1512 env::set_current_dir(temp_dir).unwrap();
1513 init_repo(temp_dir);
1514 let config_path = temp_dir.join(".gitperfconfig");
1515 fs::write(
1516 &config_path,
1517 "[environment]\nfoo = \"GITPERF_TEST_DEFINITELY_NOT_SET_XYZ\"\n[defaults]\nfoo = \"fallback\"\n",
1518 )
1519 .unwrap();
1520 env::remove_var("GITPERF_TEST_DEFINITELY_NOT_SET_XYZ");
1521 let result = resolve_key_values(&[], false);
1522 assert!(result.contains(&("foo".to_string(), "fallback".to_string())));
1523 });
1524 }
1525
1526 #[test]
1527 fn test_resolve_key_values_multi_source_first_wins() {
1528 with_isolated_home(|temp_dir| {
1529 env::set_current_dir(temp_dir).unwrap();
1530 init_repo(temp_dir);
1531 let config_path = temp_dir.join(".gitperfconfig");
1532 fs::write(
1533 &config_path,
1534 "[environment]\nrunner_id = [\"GITPERF_MULTI_R1\", \"GITPERF_MULTI_R2\"]\n",
1535 )
1536 .unwrap();
1537 env::set_var("GITPERF_MULTI_R1", "runner1");
1538 env::set_var("GITPERF_MULTI_R2", "runner2");
1539 let result = resolve_key_values(&[], false);
1540 env::remove_var("GITPERF_MULTI_R1");
1541 env::remove_var("GITPERF_MULTI_R2");
1542 assert!(result.contains(&("runner_id".to_string(), "runner1".to_string())));
1543 assert!(!result.contains(&("runner_id".to_string(), "runner2".to_string())));
1544 });
1545 }
1546
1547 #[test]
1548 fn test_resolve_key_values_multi_source_fallback() {
1549 with_isolated_home(|temp_dir| {
1550 env::set_current_dir(temp_dir).unwrap();
1551 init_repo(temp_dir);
1552 let config_path = temp_dir.join(".gitperfconfig");
1553 fs::write(
1554 &config_path,
1555 "[environment]\nrunner_id = [\"GITPERF_FALLBACK_R1\", \"GITPERF_FALLBACK_R2\"]\n",
1556 )
1557 .unwrap();
1558 env::remove_var("GITPERF_FALLBACK_R1");
1559 env::set_var("GITPERF_FALLBACK_R2", "runner2");
1560 let result = resolve_key_values(&[], false);
1561 env::remove_var("GITPERF_FALLBACK_R2");
1562 assert!(result.contains(&("runner_id".to_string(), "runner2".to_string())));
1563 });
1564 }
1565
1566 #[test]
1567 fn test_resolve_key_values_skip_env_uses_defaults() {
1568 with_isolated_home(|temp_dir| {
1569 env::set_current_dir(temp_dir).unwrap();
1570 init_repo(temp_dir);
1571 let config_path = temp_dir.join(".gitperfconfig");
1572 fs::write(
1573 &config_path,
1574 "[environment]\nfoo = \"GITPERF_TEST_SKIP_ENV\"\n[defaults]\nfoo = \"from_defaults\"\n",
1575 )
1576 .unwrap();
1577 env::set_var("GITPERF_TEST_SKIP_ENV", "from_env");
1578 let result = resolve_key_values(&[], true);
1579 env::remove_var("GITPERF_TEST_SKIP_ENV");
1580 assert!(result.contains(&("foo".to_string(), "from_defaults".to_string())));
1581 assert!(!result.contains(&("foo".to_string(), "from_env".to_string())));
1582 });
1583 }
1584
1585 #[test]
1586 fn test_resolve_key_values_no_config_empty() {
1587 with_isolated_home(|temp_dir| {
1588 env::set_current_dir(temp_dir).unwrap();
1589 init_repo(temp_dir);
1590 let result = resolve_key_values(&[], false);
1591 assert!(result.is_empty());
1592 });
1593 }
1594
1595 #[test]
1596 fn test_resolve_key_values_allows_normal_var() {
1597 with_isolated_home(|temp_dir| {
1598 env::set_current_dir(temp_dir).unwrap();
1599 init_repo(temp_dir);
1600 let config_path = temp_dir.join(".gitperfconfig");
1601 fs::write(
1602 &config_path,
1603 "[environment]\ncommit = \"GITPERF_TEST_SHA_NORMAL\"\n",
1604 )
1605 .unwrap();
1606 env::set_var("GITPERF_TEST_SHA_NORMAL", "abc123");
1607 let result = resolve_key_values(&[], false);
1608 env::remove_var("GITPERF_TEST_SHA_NORMAL");
1609 assert!(result.contains(&("commit".to_string(), "abc123".to_string())));
1610 });
1611 }
1612
1613 #[test]
1614 fn test_resolve_key_values_empty_env_var_not_used() {
1615 with_isolated_home(|temp_dir| {
1616 env::set_current_dir(temp_dir).unwrap();
1617 init_repo(temp_dir);
1618 let config_path = temp_dir.join(".gitperfconfig");
1619 fs::write(
1620 &config_path,
1621 "[environment]\nfoo = \"GITPERF_TEST_EMPTY_VAR\"\n[defaults]\nfoo = \"fallback\"\n",
1622 )
1623 .unwrap();
1624 env::set_var("GITPERF_TEST_EMPTY_VAR", "");
1625 let result = resolve_key_values(&[], false);
1626 env::remove_var("GITPERF_TEST_EMPTY_VAR");
1627 assert!(result.contains(&("foo".to_string(), "fallback".to_string())));
1628 });
1629 }
1630
1631 #[test]
1632 fn test_measurement_unit_no_parent_default() {
1633 with_isolated_home(|temp_dir| {
1634 env::set_current_dir(temp_dir).unwrap();
1636 init_repo(temp_dir);
1637
1638 let workspace_config_path = temp_dir.join(".gitperfconfig");
1640 let local_config = r#"
1641[measurement."build_time"]
1642unit = "ms"
1643
1644[measurement."memory_usage"]
1645unit = "bytes"
1646"#;
1647 fs::write(&workspace_config_path, local_config).unwrap();
1648
1649 assert_eq!(
1651 super::measurement_unit("build_time"),
1652 Some("ms".to_string())
1653 );
1654 assert_eq!(
1655 super::measurement_unit("memory_usage"),
1656 Some("bytes".to_string())
1657 );
1658
1659 assert_eq!(super::measurement_unit("other_measurement"), None);
1661 });
1662 }
1663
1664 #[test]
1665 fn test_measurement_unit_special_chars_in_name() {
1666 with_isolated_home(|temp_dir| {
1667 env::set_current_dir(temp_dir).unwrap();
1668 init_repo(temp_dir);
1669
1670 let workspace_config_path = temp_dir.join(".gitperfconfig");
1671 let local_config = r#"
1672[measurement."with_colon::name"]
1673unit = "ns"
1674
1675[measurement."with/slash"]
1676unit = "ms"
1677
1678[measurement."bench::add_measurements/add_measurement/1::median"]
1679unit = "ns"
1680min_measurements = 3
1681"#;
1682 fs::write(&workspace_config_path, local_config).unwrap();
1683
1684 assert_eq!(
1685 super::measurement_unit("with_colon::name"),
1686 Some("ns".to_string()),
1687 "double-colon in name"
1688 );
1689 assert_eq!(
1690 super::measurement_unit("with/slash"),
1691 Some("ms".to_string()),
1692 "slash in name"
1693 );
1694 assert_eq!(
1695 super::measurement_unit("bench::add_measurements/add_measurement/1::median"),
1696 Some("ns".to_string()),
1697 "full benchmark name"
1698 );
1699 });
1700 }
1701}