1use anyhow::Result;
2use config::{Config, ConfigError, File, FileFormat};
3use std::{
4 env,
5 fs::File as StdFile,
6 io::{Read, Write},
7 path::{Path, PathBuf},
8};
9use toml_edit::{value, Document, Item, Table};
10
11use crate::git::git_interop::{get_head_revision, get_repository_root};
12
13use git_perf_cli_types::DispersionMethod;
15
16pub trait ConfigParentFallbackExt {
21 fn get_with_parent_fallback(&self, parent: &str, name: &str, key: &str) -> Option<String>;
27}
28
29impl ConfigParentFallbackExt for Config {
30 fn get_with_parent_fallback(&self, parent: &str, name: &str, key: &str) -> Option<String> {
31 let specific_key = format!("{}.{}.{}", parent, name, key);
33 if let Ok(v) = self.get_string(&specific_key) {
34 return Some(v);
35 }
36
37 let parent_key = format!("{}.{}", parent, key);
39 if let Ok(v) = self.get_string(&parent_key) {
40 return Some(v);
41 }
42
43 None
44 }
45}
46
47fn get_main_config_path() -> Result<PathBuf> {
49 let repo_root = get_repository_root().map_err(|e| {
51 anyhow::anyhow!(
52 "Failed to determine repository root - must be run from within a git repository: {}",
53 e
54 )
55 })?;
56
57 if repo_root.is_empty() {
58 return Err(anyhow::anyhow!(
59 "Repository root is empty - must be run from within a git repository"
60 ));
61 }
62
63 Ok(PathBuf::from(repo_root).join(".gitperfconfig"))
64}
65
66pub fn write_config(conf: &str) -> Result<()> {
68 let path = get_main_config_path()?;
69 let mut f = StdFile::create(path)?;
70 f.write_all(conf.as_bytes())?;
71 Ok(())
72}
73
74pub fn read_hierarchical_config() -> Result<Config, ConfigError> {
76 let mut builder = Config::builder();
77
78 if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
80 let system_config_path = Path::new(&xdg_config_home)
81 .join("git-perf")
82 .join("config.toml");
83 builder = builder.add_source(
84 File::from(system_config_path)
85 .format(FileFormat::Toml)
86 .required(false),
87 );
88 } else if let Some(home) = dirs_next::home_dir() {
89 let system_config_path = home.join(".config").join("git-perf").join("config.toml");
90 builder = builder.add_source(
91 File::from(system_config_path)
92 .format(FileFormat::Toml)
93 .required(false),
94 );
95 }
96
97 if let Some(local_path) = find_config_path() {
99 builder = builder.add_source(
100 File::from(local_path)
101 .format(FileFormat::Toml)
102 .required(false),
103 );
104 }
105
106 builder.build()
107}
108
109fn find_config_path() -> Option<PathBuf> {
110 let path = get_main_config_path().ok()?;
112 if path.is_file() {
113 Some(path)
114 } else {
115 None
116 }
117}
118
119fn read_config_from_file<P: AsRef<Path>>(file: P) -> Result<String> {
120 let mut conf_str = String::new();
121 StdFile::open(file)?.read_to_string(&mut conf_str)?;
122 Ok(conf_str)
123}
124
125pub fn determine_epoch_from_config(measurement: &str) -> Option<u32> {
126 let config = read_hierarchical_config()
127 .map_err(|e| {
128 log::debug!("Could not read hierarchical config: {}", e);
130 })
131 .ok()?;
132
133 config
135 .get_with_parent_fallback("measurement", measurement, "epoch")
136 .and_then(|s| u32::from_str_radix(&s, 16).ok())
137}
138
139pub fn bump_epoch_in_conf(measurement: &str, conf_str: &mut String) -> Result<()> {
140 let mut conf = conf_str
141 .parse::<Document>()
142 .map_err(|e| anyhow::anyhow!("Failed to parse config: {}", e))?;
143
144 let head_revision = get_head_revision()?;
145
146 if !conf.contains_key("measurement") {
148 conf["measurement"] = Item::Table(Table::new());
149 }
150 if !conf["measurement"]
151 .as_table()
152 .unwrap()
153 .contains_key(measurement)
154 {
155 conf["measurement"][measurement] = Item::Table(Table::new());
156 }
157
158 conf["measurement"][measurement]["epoch"] = value(&head_revision[0..8]);
159 *conf_str = conf.to_string();
160
161 Ok(())
162}
163
164pub fn bump_epoch(measurement: &str) -> Result<()> {
165 let config_path = get_main_config_path()?;
167 let mut conf_str = read_config_from_file(&config_path).unwrap_or_default();
168
169 bump_epoch_in_conf(measurement, &mut conf_str)?;
170 write_config(&conf_str)?;
171 Ok(())
172}
173
174pub fn backoff_max_elapsed_seconds() -> u64 {
176 match read_hierarchical_config() {
177 Ok(config) => {
178 if let Ok(seconds) = config.get_int("backoff.max_elapsed_seconds") {
179 seconds as u64
180 } else {
181 60 }
183 }
184 Err(_) => 60, }
186}
187
188pub fn audit_min_relative_deviation(measurement: &str) -> Option<f64> {
190 let config = read_hierarchical_config().ok()?;
191
192 if let Some(s) =
193 config.get_with_parent_fallback("measurement", measurement, "min_relative_deviation")
194 {
195 if let Ok(v) = s.parse::<f64>() {
196 return Some(v);
197 }
198 }
199
200 None
201}
202
203pub fn audit_dispersion_method(measurement: &str) -> DispersionMethod {
205 let Some(config) = read_hierarchical_config().ok() else {
206 return DispersionMethod::StandardDeviation;
207 };
208
209 if let Some(s) =
210 config.get_with_parent_fallback("measurement", measurement, "dispersion_method")
211 {
212 if let Ok(method) = s.parse::<DispersionMethod>() {
213 return method;
214 }
215 }
216
217 DispersionMethod::StandardDeviation
219}
220
221pub fn audit_min_measurements(measurement: &str) -> Option<u16> {
223 let config = read_hierarchical_config().ok()?;
224
225 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "min_measurements")
226 {
227 if let Ok(v) = s.parse::<u16>() {
228 return Some(v);
229 }
230 }
231
232 None
233}
234
235pub fn audit_aggregate_by(measurement: &str) -> Option<git_perf_cli_types::ReductionFunc> {
237 let config = read_hierarchical_config().ok()?;
238
239 let s = config.get_with_parent_fallback("measurement", measurement, "aggregate_by")?;
240
241 match s.to_lowercase().as_str() {
243 "min" => Some(git_perf_cli_types::ReductionFunc::Min),
244 "max" => Some(git_perf_cli_types::ReductionFunc::Max),
245 "median" => Some(git_perf_cli_types::ReductionFunc::Median),
246 "mean" => Some(git_perf_cli_types::ReductionFunc::Mean),
247 _ => None,
248 }
249}
250
251pub fn audit_sigma(measurement: &str) -> Option<f64> {
253 let config = read_hierarchical_config().ok()?;
254
255 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "sigma") {
256 if let Ok(v) = s.parse::<f64>() {
257 return Some(v);
258 }
259 }
260
261 None
262}
263
264pub fn measurement_unit(measurement: &str) -> Option<String> {
266 let config = read_hierarchical_config().ok()?;
267 config.get_with_parent_fallback("measurement", measurement, "unit")
268}
269
270#[cfg(test)]
271mod test {
272 use super::*;
273 use crate::test_helpers::{
274 hermetic_git_env, init_repo, init_repo_with_file, with_isolated_home,
275 };
276 use std::fs;
277 use tempfile::TempDir;
278
279 fn create_home_config_dir(home_dir: &Path) -> PathBuf {
281 let config_dir = home_dir.join(".config").join("git-perf");
282 fs::create_dir_all(&config_dir).unwrap();
283 config_dir.join("config.toml")
284 }
285
286 #[test]
287 fn test_read_epochs() {
288 with_isolated_home(|temp_dir| {
289 env::set_current_dir(temp_dir).unwrap();
291 init_repo(temp_dir);
292
293 let workspace_config_path = temp_dir.join(".gitperfconfig");
295 let configfile = r#"[measurement]
296# General performance regression
297epoch="12344555"
298
299[measurement."something"]
300#My comment
301epoch="34567898"
302
303[measurement."somethingelse"]
304epoch="a3dead"
305"#;
306 fs::write(&workspace_config_path, configfile).unwrap();
307
308 let epoch = determine_epoch_from_config("something");
309 assert_eq!(epoch, Some(0x34567898));
310
311 let epoch = determine_epoch_from_config("somethingelse");
312 assert_eq!(epoch, Some(0xa3dead));
313
314 let epoch = determine_epoch_from_config("unspecified");
315 assert_eq!(epoch, Some(0x12344555));
316 });
317 }
318
319 #[test]
320 fn test_bump_epochs() {
321 with_isolated_home(|temp_dir| {
322 env::set_current_dir(temp_dir).unwrap();
324
325 hermetic_git_env();
327
328 init_repo_with_file(temp_dir);
330
331 let configfile = r#"[measurement."something"]
332#My comment
333epoch = "34567898"
334"#;
335
336 let mut actual = String::from(configfile);
337 bump_epoch_in_conf("something", &mut actual).expect("Failed to bump epoch");
338
339 let expected = format!(
340 r#"[measurement."something"]
341#My comment
342epoch = "{}"
343"#,
344 &get_head_revision().expect("get_head_revision failed")[0..8],
345 );
346
347 assert_eq!(actual, expected);
348 });
349 }
350
351 #[test]
352 fn test_bump_new_epoch_and_read_it() {
353 with_isolated_home(|temp_dir| {
354 env::set_current_dir(temp_dir).unwrap();
356
357 hermetic_git_env();
359
360 init_repo_with_file(temp_dir);
362
363 let mut conf = String::new();
364 bump_epoch_in_conf("mymeasurement", &mut conf).expect("Failed to bump epoch");
365
366 let config_path = temp_dir.join(".gitperfconfig");
368 fs::write(&config_path, &conf).unwrap();
369
370 let epoch = determine_epoch_from_config("mymeasurement");
371 assert!(epoch.is_some());
372 });
373 }
374
375 #[test]
376 fn test_backoff_max_elapsed_seconds() {
377 with_isolated_home(|temp_dir| {
378 env::set_current_dir(temp_dir).unwrap();
380 init_repo(temp_dir);
381
382 let workspace_config_path = temp_dir.join(".gitperfconfig");
384 let local_config = "[backoff]\nmax_elapsed_seconds = 42\n";
385 fs::write(&workspace_config_path, local_config).unwrap();
386
387 assert_eq!(super::backoff_max_elapsed_seconds(), 42);
389
390 fs::remove_file(&workspace_config_path).unwrap();
392 assert_eq!(super::backoff_max_elapsed_seconds(), 60);
393 });
394 }
395
396 #[test]
397 fn test_audit_min_relative_deviation() {
398 with_isolated_home(|temp_dir| {
399 env::set_current_dir(temp_dir).unwrap();
401 init_repo(temp_dir);
402
403 let workspace_config_path = temp_dir.join(".gitperfconfig");
405 let local_config = r#"
406[measurement]
407min_relative_deviation = 5.0
408
409[measurement."build_time"]
410min_relative_deviation = 10.0
411
412[measurement."memory_usage"]
413min_relative_deviation = 2.5
414"#;
415 fs::write(&workspace_config_path, local_config).unwrap();
416
417 assert_eq!(
419 super::audit_min_relative_deviation("build_time"),
420 Some(10.0)
421 );
422 assert_eq!(
423 super::audit_min_relative_deviation("memory_usage"),
424 Some(2.5)
425 );
426 assert_eq!(
427 super::audit_min_relative_deviation("other_measurement"),
428 Some(5.0) );
430
431 let global_config = r#"
433[measurement]
434min_relative_deviation = 5.0
435"#;
436 fs::write(&workspace_config_path, global_config).unwrap();
437 assert_eq!(
438 super::audit_min_relative_deviation("any_measurement"),
439 Some(5.0)
440 );
441
442 let precedence_config = r#"
444[measurement]
445min_relative_deviation = 5.0
446
447[measurement."build_time"]
448min_relative_deviation = 10.0
449"#;
450 fs::write(&workspace_config_path, precedence_config).unwrap();
451 assert_eq!(
452 super::audit_min_relative_deviation("build_time"),
453 Some(10.0)
454 );
455 assert_eq!(
456 super::audit_min_relative_deviation("other_measurement"),
457 Some(5.0)
458 );
459
460 fs::remove_file(&workspace_config_path).unwrap();
462 assert_eq!(super::audit_min_relative_deviation("any_measurement"), None);
463 });
464 }
465
466 #[test]
467 fn test_audit_dispersion_method() {
468 with_isolated_home(|temp_dir| {
469 env::set_current_dir(temp_dir).unwrap();
471 init_repo(temp_dir);
472
473 let workspace_config_path = temp_dir.join(".gitperfconfig");
475 let local_config = r#"
476[measurement]
477dispersion_method = "stddev"
478
479[measurement."build_time"]
480dispersion_method = "mad"
481
482[measurement."memory_usage"]
483dispersion_method = "stddev"
484"#;
485 fs::write(&workspace_config_path, local_config).unwrap();
486
487 assert_eq!(
489 super::audit_dispersion_method("build_time"),
490 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
491 );
492 assert_eq!(
493 super::audit_dispersion_method("memory_usage"),
494 git_perf_cli_types::DispersionMethod::StandardDeviation
495 );
496 assert_eq!(
497 super::audit_dispersion_method("other_measurement"),
498 git_perf_cli_types::DispersionMethod::StandardDeviation
499 );
500
501 let global_config = r#"
503[measurement]
504dispersion_method = "mad"
505"#;
506 fs::write(&workspace_config_path, global_config).unwrap();
507 assert_eq!(
508 super::audit_dispersion_method("any_measurement"),
509 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
510 );
511
512 let precedence_config = r#"
514[measurement]
515dispersion_method = "mad"
516
517[measurement."build_time"]
518dispersion_method = "stddev"
519"#;
520 fs::write(&workspace_config_path, precedence_config).unwrap();
521 assert_eq!(
522 super::audit_dispersion_method("build_time"),
523 git_perf_cli_types::DispersionMethod::StandardDeviation
524 );
525 assert_eq!(
526 super::audit_dispersion_method("other_measurement"),
527 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
528 );
529
530 fs::remove_file(&workspace_config_path).unwrap();
532 assert_eq!(
533 super::audit_dispersion_method("any_measurement"),
534 git_perf_cli_types::DispersionMethod::StandardDeviation
535 );
536 });
537 }
538
539 #[test]
540 fn test_bump_epoch_in_conf_creates_proper_tables() {
541 with_isolated_home(|temp_dir| {
544 env::set_current_dir(temp_dir).unwrap();
545
546 hermetic_git_env();
548
549 init_repo_with_file(temp_dir);
550
551 let mut empty_config = String::new();
553
554 bump_epoch_in_conf("mymeasurement", &mut empty_config).unwrap();
556
557 assert!(empty_config.contains("[measurement]"));
559 assert!(empty_config.contains("[measurement.mymeasurement]"));
560 assert!(empty_config.contains("epoch ="));
561 assert!(!empty_config.contains("measurement = {"));
563 assert!(!empty_config.contains("mymeasurement = {"));
564
565 let mut existing_config = r#"[measurement]
567existing_setting = "value"
568
569[measurement."other"]
570epoch = "oldvalue"
571"#
572 .to_string();
573
574 bump_epoch_in_conf("newmeasurement", &mut existing_config).unwrap();
575
576 assert!(existing_config.contains("[measurement.newmeasurement]"));
578 assert!(existing_config.contains("existing_setting = \"value\""));
579 assert!(existing_config.contains("[measurement.\"other\"]"));
580 assert!(!existing_config.contains("newmeasurement = {"));
581 });
582 }
583
584 #[test]
585 fn test_find_config_path_in_git_root() {
586 with_isolated_home(|temp_dir| {
587 env::set_current_dir(temp_dir).unwrap();
589
590 init_repo(temp_dir);
592
593 let config_path = temp_dir.join(".gitperfconfig");
595 fs::write(
596 &config_path,
597 "[measurement.\"test\"]\nepoch = \"12345678\"\n",
598 )
599 .unwrap();
600
601 let found_path = find_config_path();
603 assert!(found_path.is_some());
604 assert_eq!(found_path.unwrap(), config_path);
605 });
606 }
607
608 #[test]
609 fn test_find_config_path_not_found() {
610 with_isolated_home(|temp_dir| {
611 env::set_current_dir(temp_dir).unwrap();
613
614 init_repo(temp_dir);
616
617 let found_path = find_config_path();
619 assert!(found_path.is_none());
620 });
621 }
622
623 #[test]
624 fn test_hierarchical_config_workspace_overrides_home() {
625 with_isolated_home(|temp_dir| {
626 env::set_current_dir(temp_dir).unwrap();
628
629 init_repo(temp_dir);
631
632 let home_config_path = create_home_config_dir(temp_dir);
634 fs::write(
635 &home_config_path,
636 r#"
637[measurement."test"]
638backoff_max_elapsed_seconds = 30
639audit_min_relative_deviation = 1.0
640"#,
641 )
642 .unwrap();
643
644 let workspace_config_path = temp_dir.join(".gitperfconfig");
646 fs::write(
647 &workspace_config_path,
648 r#"
649[measurement."test"]
650backoff_max_elapsed_seconds = 60
651"#,
652 )
653 .unwrap();
654
655 env::set_var("HOME", temp_dir);
657 env::remove_var("XDG_CONFIG_HOME");
658
659 let config = read_hierarchical_config().unwrap();
661
662 let backoff: i32 = config
664 .get("measurement.test.backoff_max_elapsed_seconds")
665 .unwrap();
666 assert_eq!(backoff, 60);
667
668 let deviation: f64 = config
670 .get("measurement.test.audit_min_relative_deviation")
671 .unwrap();
672 assert_eq!(deviation, 1.0);
673 });
674 }
675
676 #[test]
677 fn test_determine_epoch_from_config_with_missing_file() {
678 let temp_dir = TempDir::new().unwrap();
680 fs::create_dir_all(temp_dir.path()).unwrap();
681 env::set_current_dir(temp_dir.path()).unwrap();
682
683 let epoch = determine_epoch_from_config("test_measurement");
684 assert!(epoch.is_none());
685 }
686
687 #[test]
688 fn test_determine_epoch_from_config_with_invalid_toml() {
689 let temp_dir = TempDir::new().unwrap();
690 let config_path = temp_dir.path().join(".gitperfconfig");
691 fs::write(&config_path, "invalid toml content").unwrap();
692
693 fs::create_dir_all(temp_dir.path()).unwrap();
694 env::set_current_dir(temp_dir.path()).unwrap();
695
696 let epoch = determine_epoch_from_config("test_measurement");
697 assert!(epoch.is_none());
698 }
699
700 #[test]
701 fn test_write_config_creates_file() {
702 with_isolated_home(|temp_dir| {
703 env::set_current_dir(temp_dir).unwrap();
705 init_repo(temp_dir);
706
707 let subdir = temp_dir.join("a").join("b").join("c");
709 fs::create_dir_all(&subdir).unwrap();
710 env::set_current_dir(&subdir).unwrap();
711
712 let config_content = "[measurement.\"test\"]\nepoch = \"12345678\"\n";
713 write_config(config_content).unwrap();
714
715 let repo_config_path = temp_dir.join(".gitperfconfig");
717 let subdir_config_path = subdir.join(".gitperfconfig");
718
719 assert!(repo_config_path.is_file());
720 assert!(!subdir_config_path.is_file());
721
722 let content = fs::read_to_string(&repo_config_path).unwrap();
723 assert_eq!(content, config_content);
724 });
725 }
726
727 #[test]
728 fn test_hierarchical_config_system_override() {
729 with_isolated_home(|temp_dir| {
730 let system_config_path = create_home_config_dir(temp_dir);
732 let system_config = r#"
733[measurement]
734min_relative_deviation = 5.0
735dispersion_method = "mad"
736
737[backoff]
738max_elapsed_seconds = 120
739"#;
740 fs::write(&system_config_path, system_config).unwrap();
741
742 env::set_current_dir(temp_dir).unwrap();
744 init_repo(temp_dir);
745
746 let workspace_config_path = temp_dir.join(".gitperfconfig");
748 let local_config = r#"
749[measurement]
750min_relative_deviation = 10.0
751
752[measurement."build_time"]
753min_relative_deviation = 15.0
754dispersion_method = "stddev"
755"#;
756 fs::write(&workspace_config_path, local_config).unwrap();
757
758 let config = read_hierarchical_config().unwrap();
760
761 use super::ConfigParentFallbackExt;
763 assert_eq!(
764 config
765 .get_with_parent_fallback(
766 "measurement",
767 "any_measurement",
768 "min_relative_deviation"
769 )
770 .unwrap()
771 .parse::<f64>()
772 .unwrap(),
773 10.0
774 );
775 assert_eq!(
776 config
777 .get_with_parent_fallback("measurement", "any_measurement", "dispersion_method")
778 .unwrap(),
779 "mad"
780 ); assert_eq!(
784 config
785 .get_float("measurement.build_time.min_relative_deviation")
786 .unwrap(),
787 15.0
788 );
789 assert_eq!(
790 config
791 .get_string("measurement.build_time.dispersion_method")
792 .unwrap(),
793 "stddev"
794 );
795
796 assert_eq!(config.get_int("backoff.max_elapsed_seconds").unwrap(), 120);
798
799 assert_eq!(audit_min_relative_deviation("build_time"), Some(15.0));
801 assert_eq!(
802 audit_min_relative_deviation("other_measurement"),
803 Some(10.0)
804 );
805 assert_eq!(
806 audit_dispersion_method("build_time"),
807 git_perf_cli_types::DispersionMethod::StandardDeviation
808 );
809 assert_eq!(
810 audit_dispersion_method("other_measurement"),
811 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
812 );
813 assert_eq!(backoff_max_elapsed_seconds(), 120);
814 });
815 }
816
817 #[test]
818 fn test_read_config_from_file_missing_file() {
819 let temp_dir = TempDir::new().unwrap();
820 let nonexistent_file = temp_dir.path().join("does_not_exist.toml");
821
822 let result = read_config_from_file(&nonexistent_file);
824 assert!(result.is_err());
825 }
826
827 #[test]
828 fn test_read_config_from_file_valid_content() {
829 let temp_dir = TempDir::new().unwrap();
830 let config_file = temp_dir.path().join("test_config.toml");
831 let expected_content = "[measurement]\nepoch = \"12345678\"\n";
832
833 fs::write(&config_file, expected_content).unwrap();
834
835 let result = read_config_from_file(&config_file);
836 assert!(result.is_ok());
837 let content = result.unwrap();
838 assert_eq!(content, expected_content);
839
840 assert!(!content.is_empty());
842 }
843
844 #[test]
845 fn test_audit_min_measurements() {
846 with_isolated_home(|temp_dir| {
847 env::set_current_dir(temp_dir).unwrap();
849 init_repo(temp_dir);
850
851 let workspace_config_path = temp_dir.join(".gitperfconfig");
853 let local_config = r#"
854[measurement]
855min_measurements = 5
856
857[measurement."build_time"]
858min_measurements = 10
859
860[measurement."memory_usage"]
861min_measurements = 3
862"#;
863 fs::write(&workspace_config_path, local_config).unwrap();
864
865 assert_eq!(super::audit_min_measurements("build_time"), Some(10));
867 assert_eq!(super::audit_min_measurements("memory_usage"), Some(3));
868 assert_eq!(super::audit_min_measurements("other_measurement"), Some(5));
869
870 fs::remove_file(&workspace_config_path).unwrap();
872 assert_eq!(super::audit_min_measurements("any_measurement"), None);
873 });
874 }
875
876 #[test]
877 fn test_audit_aggregate_by() {
878 with_isolated_home(|temp_dir| {
879 env::set_current_dir(temp_dir).unwrap();
881 init_repo(temp_dir);
882
883 let workspace_config_path = temp_dir.join(".gitperfconfig");
885 let local_config = r#"
886[measurement]
887aggregate_by = "median"
888
889[measurement."build_time"]
890aggregate_by = "max"
891
892[measurement."memory_usage"]
893aggregate_by = "mean"
894"#;
895 fs::write(&workspace_config_path, local_config).unwrap();
896
897 assert_eq!(
899 super::audit_aggregate_by("build_time"),
900 Some(git_perf_cli_types::ReductionFunc::Max)
901 );
902 assert_eq!(
903 super::audit_aggregate_by("memory_usage"),
904 Some(git_perf_cli_types::ReductionFunc::Mean)
905 );
906 assert_eq!(
907 super::audit_aggregate_by("other_measurement"),
908 Some(git_perf_cli_types::ReductionFunc::Median)
909 );
910
911 fs::remove_file(&workspace_config_path).unwrap();
913 assert_eq!(super::audit_aggregate_by("any_measurement"), None);
914 });
915 }
916
917 #[test]
918 fn test_audit_sigma() {
919 with_isolated_home(|temp_dir| {
920 env::set_current_dir(temp_dir).unwrap();
922 init_repo(temp_dir);
923
924 let workspace_config_path = temp_dir.join(".gitperfconfig");
926 let local_config = r#"
927[measurement]
928sigma = 3.0
929
930[measurement."build_time"]
931sigma = 5.5
932
933[measurement."memory_usage"]
934sigma = 2.0
935"#;
936 fs::write(&workspace_config_path, local_config).unwrap();
937
938 assert_eq!(super::audit_sigma("build_time"), Some(5.5));
940 assert_eq!(super::audit_sigma("memory_usage"), Some(2.0));
941 assert_eq!(super::audit_sigma("other_measurement"), Some(3.0));
942
943 fs::remove_file(&workspace_config_path).unwrap();
945 assert_eq!(super::audit_sigma("any_measurement"), None);
946 });
947 }
948
949 #[test]
950 fn test_measurement_unit() {
951 with_isolated_home(|temp_dir| {
952 env::set_current_dir(temp_dir).unwrap();
954 init_repo(temp_dir);
955
956 let workspace_config_path = temp_dir.join(".gitperfconfig");
958 let local_config = r#"
959[measurement]
960unit = "ms"
961
962[measurement."build_time"]
963unit = "ms"
964
965[measurement."memory_usage"]
966unit = "bytes"
967
968[measurement."throughput"]
969unit = "requests/sec"
970"#;
971 fs::write(&workspace_config_path, local_config).unwrap();
972
973 assert_eq!(
975 super::measurement_unit("build_time"),
976 Some("ms".to_string())
977 );
978 assert_eq!(
979 super::measurement_unit("memory_usage"),
980 Some("bytes".to_string())
981 );
982 assert_eq!(
983 super::measurement_unit("throughput"),
984 Some("requests/sec".to_string())
985 );
986
987 assert_eq!(
989 super::measurement_unit("other_measurement"),
990 Some("ms".to_string())
991 );
992
993 fs::remove_file(&workspace_config_path).unwrap();
995 assert_eq!(super::measurement_unit("any_measurement"), None);
996 });
997 }
998
999 #[test]
1000 fn test_measurement_unit_precedence() {
1001 with_isolated_home(|temp_dir| {
1002 env::set_current_dir(temp_dir).unwrap();
1004 init_repo(temp_dir);
1005
1006 let workspace_config_path = temp_dir.join(".gitperfconfig");
1008 let precedence_config = r#"
1009[measurement]
1010unit = "ms"
1011
1012[measurement."build_time"]
1013unit = "seconds"
1014"#;
1015 fs::write(&workspace_config_path, precedence_config).unwrap();
1016
1017 assert_eq!(
1019 super::measurement_unit("build_time"),
1020 Some("seconds".to_string())
1021 );
1022
1023 assert_eq!(
1025 super::measurement_unit("other_measurement"),
1026 Some("ms".to_string())
1027 );
1028 });
1029 }
1030
1031 #[test]
1032 fn test_measurement_unit_no_parent_default() {
1033 with_isolated_home(|temp_dir| {
1034 env::set_current_dir(temp_dir).unwrap();
1036 init_repo(temp_dir);
1037
1038 let workspace_config_path = temp_dir.join(".gitperfconfig");
1040 let local_config = r#"
1041[measurement."build_time"]
1042unit = "ms"
1043
1044[measurement."memory_usage"]
1045unit = "bytes"
1046"#;
1047 fs::write(&workspace_config_path, local_config).unwrap();
1048
1049 assert_eq!(
1051 super::measurement_unit("build_time"),
1052 Some("ms".to_string())
1053 );
1054 assert_eq!(
1055 super::measurement_unit("memory_usage"),
1056 Some("bytes".to_string())
1057 );
1058
1059 assert_eq!(super::measurement_unit("other_measurement"), None);
1061 });
1062 }
1063}