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, DocumentMut, Item, Table};
10
11use crate::defaults;
12use crate::git::git_interop::{get_head_revision, get_repository_root};
13
14use git_perf_cli_types::DispersionMethod;
16
17pub trait ConfigParentFallbackExt {
22 fn get_with_parent_fallback(&self, parent: &str, name: &str, key: &str) -> Option<String>;
28}
29
30impl ConfigParentFallbackExt for Config {
31 fn get_with_parent_fallback(&self, parent: &str, name: &str, key: &str) -> Option<String> {
32 let specific_key = format!("{}.{}.{}", parent, name, key);
34 if let Ok(v) = self.get_string(&specific_key) {
35 return Some(v);
36 }
37
38 let parent_key = format!("{}.{}", parent, key);
40 if let Ok(v) = self.get_string(&parent_key) {
41 return Some(v);
42 }
43
44 None
45 }
46}
47
48fn get_main_config_path() -> Result<PathBuf> {
50 let repo_root = get_repository_root().map_err(|e| {
52 anyhow::anyhow!(
53 "Failed to determine repository root - must be run from within a git repository: {}",
54 e
55 )
56 })?;
57
58 if repo_root.is_empty() {
59 return Err(anyhow::anyhow!(
60 "Repository root is empty - must be run from within a git repository"
61 ));
62 }
63
64 Ok(PathBuf::from(repo_root).join(".gitperfconfig"))
65}
66
67pub fn write_config(conf: &str) -> Result<()> {
69 let path = get_main_config_path()?;
70 let mut f = StdFile::create(path)?;
71 f.write_all(conf.as_bytes())?;
72 Ok(())
73}
74
75pub fn read_hierarchical_config() -> Result<Config, ConfigError> {
77 let mut builder = Config::builder();
78
79 if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
81 let system_config_path = Path::new(&xdg_config_home)
82 .join("git-perf")
83 .join("config.toml");
84 builder = builder.add_source(
85 File::from(system_config_path)
86 .format(FileFormat::Toml)
87 .required(false),
88 );
89 } else if let Some(home) = dirs_next::home_dir() {
90 let system_config_path = home.join(".config").join("git-perf").join("config.toml");
91 builder = builder.add_source(
92 File::from(system_config_path)
93 .format(FileFormat::Toml)
94 .required(false),
95 );
96 }
97
98 if let Some(local_path) = find_config_path() {
100 builder = builder.add_source(
101 File::from(local_path)
102 .format(FileFormat::Toml)
103 .required(false),
104 );
105 }
106
107 builder.build()
108}
109
110fn find_config_path() -> Option<PathBuf> {
111 let path = get_main_config_path().ok()?;
113 if path.is_file() {
114 Some(path)
115 } else {
116 None
117 }
118}
119
120fn read_config_from_file<P: AsRef<Path>>(file: P) -> Result<String> {
121 let mut conf_str = String::new();
122 StdFile::open(file)?.read_to_string(&mut conf_str)?;
123 Ok(conf_str)
124}
125
126#[must_use]
127pub fn determine_epoch_from_config(measurement: &str) -> Option<u32> {
128 let config = read_hierarchical_config()
129 .map_err(|e| {
130 log::debug!("Could not read hierarchical config: {}", e);
132 })
133 .ok()?;
134
135 config
137 .get_with_parent_fallback("measurement", measurement, "epoch")
138 .and_then(|s| u32::from_str_radix(&s, 16).ok())
139}
140
141pub fn bump_epoch_in_conf(measurement: &str, conf_str: &mut String) -> Result<()> {
142 let mut conf = conf_str
143 .parse::<DocumentMut>()
144 .map_err(|e| anyhow::anyhow!("Failed to parse config: {}", e))?;
145
146 let head_revision = get_head_revision()?;
147
148 if !conf.contains_key("measurement") {
150 conf["measurement"] = Item::Table(Table::new());
151 }
152 if !conf["measurement"]
153 .as_table()
154 .unwrap()
155 .contains_key(measurement)
156 {
157 conf["measurement"][measurement] = Item::Table(Table::new());
158 }
159
160 conf["measurement"][measurement]["epoch"] = value(&head_revision[0..8]);
161 *conf_str = conf.to_string();
162
163 Ok(())
164}
165
166pub fn bump_epoch(measurement: &str) -> Result<()> {
167 let config_path = get_main_config_path()?;
169 let mut conf_str = read_config_from_file(&config_path).unwrap_or_default();
170
171 bump_epoch_in_conf(measurement, &mut conf_str)?;
172 write_config(&conf_str)?;
173 Ok(())
174}
175
176#[must_use]
178pub fn backoff_max_elapsed_seconds() -> u64 {
179 match read_hierarchical_config() {
180 Ok(config) => {
181 if let Ok(seconds) = config.get_int("backoff.max_elapsed_seconds") {
182 seconds as u64
183 } else {
184 defaults::DEFAULT_BACKOFF_MAX_ELAPSED_SECONDS
185 }
186 }
187 Err(_) => defaults::DEFAULT_BACKOFF_MAX_ELAPSED_SECONDS,
188 }
189}
190
191#[must_use]
193pub fn audit_min_relative_deviation(measurement: &str) -> Option<f64> {
194 let config = read_hierarchical_config().ok()?;
195
196 if let Some(s) =
197 config.get_with_parent_fallback("measurement", measurement, "min_relative_deviation")
198 {
199 if let Ok(v) = s.parse::<f64>() {
200 return Some(v);
201 }
202 }
203
204 None
205}
206
207#[must_use]
209pub fn audit_min_absolute_deviation(measurement: &str) -> Option<f64> {
210 let config = read_hierarchical_config().ok()?;
211
212 if let Some(s) =
213 config.get_with_parent_fallback("measurement", measurement, "min_absolute_deviation")
214 {
215 if let Ok(v) = s.parse::<f64>() {
216 return Some(v);
217 }
218 }
219
220 None
221}
222
223#[must_use]
225pub fn audit_dispersion_method(measurement: &str) -> DispersionMethod {
226 let Some(config) = read_hierarchical_config().ok() else {
227 return DispersionMethod::StandardDeviation;
228 };
229
230 if let Some(s) =
231 config.get_with_parent_fallback("measurement", measurement, "dispersion_method")
232 {
233 if let Ok(method) = s.parse::<DispersionMethod>() {
234 return method;
235 }
236 }
237
238 DispersionMethod::StandardDeviation
239}
240
241#[must_use]
243pub fn audit_min_measurements(measurement: &str) -> Option<u16> {
244 let config = read_hierarchical_config().ok()?;
245
246 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "min_measurements")
247 {
248 if let Ok(v) = s.parse::<u16>() {
249 return Some(v);
250 }
251 }
252
253 None
254}
255
256#[must_use]
258pub fn audit_aggregate_by(measurement: &str) -> Option<git_perf_cli_types::ReductionFunc> {
259 let config = read_hierarchical_config().ok()?;
260
261 let s = config.get_with_parent_fallback("measurement", measurement, "aggregate_by")?;
262
263 match s.to_lowercase().as_str() {
265 "min" => Some(git_perf_cli_types::ReductionFunc::Min),
266 "max" => Some(git_perf_cli_types::ReductionFunc::Max),
267 "median" => Some(git_perf_cli_types::ReductionFunc::Median),
268 "mean" => Some(git_perf_cli_types::ReductionFunc::Mean),
269 _ => None,
270 }
271}
272
273#[must_use]
275pub fn audit_sigma(measurement: &str) -> Option<f64> {
276 let config = read_hierarchical_config().ok()?;
277
278 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "sigma") {
279 if let Ok(v) = s.parse::<f64>() {
280 return Some(v);
281 }
282 }
283
284 None
285}
286
287#[must_use]
289pub fn measurement_unit(measurement: &str) -> Option<String> {
290 let config = read_hierarchical_config().ok()?;
291 config.get_with_parent_fallback("measurement", measurement, "unit")
292}
293
294#[must_use]
296pub fn report_template_path() -> Option<PathBuf> {
297 let config = read_hierarchical_config().ok()?;
298 let path_str = config.get_string("report.template_path").ok()?;
299 Some(PathBuf::from(path_str))
300}
301
302#[must_use]
304pub fn report_custom_css_path() -> Option<PathBuf> {
305 let config = read_hierarchical_config().ok()?;
306 let path_str = config.get_string("report.custom_css_path").ok()?;
307 Some(PathBuf::from(path_str))
308}
309
310#[must_use]
312pub fn report_title() -> Option<String> {
313 let config = read_hierarchical_config().ok()?;
314 config.get_string("report.title").ok()
315}
316
317#[must_use]
326pub fn change_point_config(measurement: &str) -> crate::change_point::ChangePointConfig {
327 let mut config = crate::change_point::ChangePointConfig::default();
328
329 let Ok(file_config) = read_hierarchical_config() else {
330 return config;
331 };
332
333 if let Some(enabled_str) =
335 file_config.get_with_parent_fallback("change_point", measurement, "enabled")
336 {
337 if let Ok(enabled) = enabled_str.parse::<bool>() {
338 config.enabled = enabled;
339 }
340 }
341
342 if let Some(s) =
344 file_config.get_with_parent_fallback("change_point", measurement, "min_data_points")
345 {
346 if let Ok(v) = s.parse::<usize>() {
347 config.min_data_points = v;
348 }
349 }
350
351 if let Some(s) =
353 file_config.get_with_parent_fallback("change_point", measurement, "min_magnitude_pct")
354 {
355 if let Ok(v) = s.parse::<f64>() {
356 config.min_magnitude_pct = v;
357 }
358 }
359
360 if let Some(s) =
362 file_config.get_with_parent_fallback("change_point", measurement, "confidence_threshold")
363 {
364 if let Ok(v) = s.parse::<f64>() {
365 config.confidence_threshold = v;
366 }
367 }
368
369 if let Some(s) = file_config.get_with_parent_fallback("change_point", measurement, "penalty") {
371 if let Ok(v) = s.parse::<f64>() {
372 config.penalty = v;
373 }
374 }
375
376 config
377}
378
379#[cfg(test)]
380mod test {
381 use super::*;
382 use crate::test_helpers::{
383 hermetic_git_env, init_repo, init_repo_with_file, with_isolated_home,
384 };
385 use std::fs;
386 use tempfile::TempDir;
387
388 fn create_home_config_dir(home_dir: &Path) -> PathBuf {
390 let config_dir = home_dir.join(".config").join("git-perf");
391 fs::create_dir_all(&config_dir).unwrap();
392 config_dir.join("config.toml")
393 }
394
395 #[test]
396 fn test_read_epochs() {
397 with_isolated_home(|temp_dir| {
398 env::set_current_dir(temp_dir).unwrap();
400 init_repo(temp_dir);
401
402 let workspace_config_path = temp_dir.join(".gitperfconfig");
404 let configfile = r#"[measurement]
405# General performance regression
406epoch="12344555"
407
408[measurement."something"]
409#My comment
410epoch="34567898"
411
412[measurement."somethingelse"]
413epoch="a3dead"
414"#;
415 fs::write(&workspace_config_path, configfile).unwrap();
416
417 let epoch = determine_epoch_from_config("something");
418 assert_eq!(epoch, Some(0x34567898));
419
420 let epoch = determine_epoch_from_config("somethingelse");
421 assert_eq!(epoch, Some(0xa3dead));
422
423 let epoch = determine_epoch_from_config("unspecified");
424 assert_eq!(epoch, Some(0x12344555));
425 });
426 }
427
428 #[test]
429 fn test_bump_epochs() {
430 with_isolated_home(|temp_dir| {
431 env::set_current_dir(temp_dir).unwrap();
433
434 hermetic_git_env();
436
437 init_repo_with_file(temp_dir);
439
440 let configfile = r#"[measurement."something"]
441#My comment
442epoch = "34567898"
443"#;
444
445 let mut actual = String::from(configfile);
446 bump_epoch_in_conf("something", &mut actual).expect("Failed to bump epoch");
447
448 let expected = format!(
449 r#"[measurement."something"]
450#My comment
451epoch = "{}"
452"#,
453 &get_head_revision().expect("get_head_revision failed")[0..8],
454 );
455
456 assert_eq!(actual, expected);
457 });
458 }
459
460 #[test]
461 fn test_bump_new_epoch_and_read_it() {
462 with_isolated_home(|temp_dir| {
463 env::set_current_dir(temp_dir).unwrap();
465
466 hermetic_git_env();
468
469 init_repo_with_file(temp_dir);
471
472 let mut conf = String::new();
473 bump_epoch_in_conf("mymeasurement", &mut conf).expect("Failed to bump epoch");
474
475 let config_path = temp_dir.join(".gitperfconfig");
477 fs::write(&config_path, &conf).unwrap();
478
479 let epoch = determine_epoch_from_config("mymeasurement");
480 assert!(epoch.is_some());
481 });
482 }
483
484 #[test]
485 fn test_backoff_max_elapsed_seconds() {
486 with_isolated_home(|temp_dir| {
487 env::set_current_dir(temp_dir).unwrap();
489 init_repo(temp_dir);
490
491 let workspace_config_path = temp_dir.join(".gitperfconfig");
493 let local_config = "[backoff]\nmax_elapsed_seconds = 42\n";
494 fs::write(&workspace_config_path, local_config).unwrap();
495
496 assert_eq!(super::backoff_max_elapsed_seconds(), 42);
498
499 fs::remove_file(&workspace_config_path).unwrap();
501 assert_eq!(super::backoff_max_elapsed_seconds(), 60);
502 });
503 }
504
505 #[test]
506 fn test_audit_min_relative_deviation() {
507 with_isolated_home(|temp_dir| {
508 env::set_current_dir(temp_dir).unwrap();
510 init_repo(temp_dir);
511
512 let workspace_config_path = temp_dir.join(".gitperfconfig");
514 let local_config = r#"
515[measurement]
516min_relative_deviation = 5.0
517
518[measurement."build_time"]
519min_relative_deviation = 10.0
520
521[measurement."memory_usage"]
522min_relative_deviation = 2.5
523"#;
524 fs::write(&workspace_config_path, local_config).unwrap();
525
526 assert_eq!(
528 super::audit_min_relative_deviation("build_time"),
529 Some(10.0)
530 );
531 assert_eq!(
532 super::audit_min_relative_deviation("memory_usage"),
533 Some(2.5)
534 );
535 assert_eq!(
536 super::audit_min_relative_deviation("other_measurement"),
537 Some(5.0) );
539
540 let global_config = r#"
542[measurement]
543min_relative_deviation = 5.0
544"#;
545 fs::write(&workspace_config_path, global_config).unwrap();
546 assert_eq!(
547 super::audit_min_relative_deviation("any_measurement"),
548 Some(5.0)
549 );
550
551 let precedence_config = r#"
553[measurement]
554min_relative_deviation = 5.0
555
556[measurement."build_time"]
557min_relative_deviation = 10.0
558"#;
559 fs::write(&workspace_config_path, precedence_config).unwrap();
560 assert_eq!(
561 super::audit_min_relative_deviation("build_time"),
562 Some(10.0)
563 );
564 assert_eq!(
565 super::audit_min_relative_deviation("other_measurement"),
566 Some(5.0)
567 );
568
569 fs::remove_file(&workspace_config_path).unwrap();
571 assert_eq!(super::audit_min_relative_deviation("any_measurement"), None);
572 });
573 }
574
575 #[test]
576 fn test_audit_min_absolute_deviation() {
577 with_isolated_home(|temp_dir| {
578 env::set_current_dir(temp_dir).unwrap();
580 init_repo(temp_dir);
581
582 let workspace_config_path = temp_dir.join(".gitperfconfig");
584 let local_config = r#"
585[measurement]
586min_absolute_deviation = 5.0
587
588[measurement."build_time"]
589min_absolute_deviation = 10.0
590
591[measurement."memory_usage"]
592min_absolute_deviation = 2.5
593"#;
594 fs::write(&workspace_config_path, local_config).unwrap();
595
596 assert_eq!(
598 super::audit_min_absolute_deviation("build_time"),
599 Some(10.0)
600 );
601 assert_eq!(
602 super::audit_min_absolute_deviation("memory_usage"),
603 Some(2.5)
604 );
605 assert_eq!(
606 super::audit_min_absolute_deviation("other_measurement"),
607 Some(5.0) );
609
610 let global_config = r#"
612[measurement]
613min_absolute_deviation = 5.0
614"#;
615 fs::write(&workspace_config_path, global_config).unwrap();
616 assert_eq!(
617 super::audit_min_absolute_deviation("any_measurement"),
618 Some(5.0)
619 );
620
621 let precedence_config = r#"
623[measurement]
624min_absolute_deviation = 5.0
625
626[measurement."build_time"]
627min_absolute_deviation = 10.0
628"#;
629 fs::write(&workspace_config_path, precedence_config).unwrap();
630 assert_eq!(
631 super::audit_min_absolute_deviation("build_time"),
632 Some(10.0)
633 );
634 assert_eq!(
635 super::audit_min_absolute_deviation("other_measurement"),
636 Some(5.0)
637 );
638
639 fs::remove_file(&workspace_config_path).unwrap();
641 assert_eq!(super::audit_min_absolute_deviation("any_measurement"), None);
642 });
643 }
644
645 #[test]
646 fn test_audit_dispersion_method() {
647 with_isolated_home(|temp_dir| {
648 env::set_current_dir(temp_dir).unwrap();
650 init_repo(temp_dir);
651
652 let workspace_config_path = temp_dir.join(".gitperfconfig");
654 let local_config = r#"
655[measurement]
656dispersion_method = "stddev"
657
658[measurement."build_time"]
659dispersion_method = "mad"
660
661[measurement."memory_usage"]
662dispersion_method = "stddev"
663"#;
664 fs::write(&workspace_config_path, local_config).unwrap();
665
666 assert_eq!(
668 super::audit_dispersion_method("build_time"),
669 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
670 );
671 assert_eq!(
672 super::audit_dispersion_method("memory_usage"),
673 git_perf_cli_types::DispersionMethod::StandardDeviation
674 );
675 assert_eq!(
676 super::audit_dispersion_method("other_measurement"),
677 git_perf_cli_types::DispersionMethod::StandardDeviation
678 );
679
680 let global_config = r#"
682[measurement]
683dispersion_method = "mad"
684"#;
685 fs::write(&workspace_config_path, global_config).unwrap();
686 assert_eq!(
687 super::audit_dispersion_method("any_measurement"),
688 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
689 );
690
691 let precedence_config = r#"
693[measurement]
694dispersion_method = "mad"
695
696[measurement."build_time"]
697dispersion_method = "stddev"
698"#;
699 fs::write(&workspace_config_path, precedence_config).unwrap();
700 assert_eq!(
701 super::audit_dispersion_method("build_time"),
702 git_perf_cli_types::DispersionMethod::StandardDeviation
703 );
704 assert_eq!(
705 super::audit_dispersion_method("other_measurement"),
706 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
707 );
708
709 fs::remove_file(&workspace_config_path).unwrap();
711 assert_eq!(
712 super::audit_dispersion_method("any_measurement"),
713 git_perf_cli_types::DispersionMethod::StandardDeviation
714 );
715 });
716 }
717
718 #[test]
719 fn test_bump_epoch_in_conf_creates_proper_tables() {
720 with_isolated_home(|temp_dir| {
723 env::set_current_dir(temp_dir).unwrap();
724
725 hermetic_git_env();
727
728 init_repo_with_file(temp_dir);
729
730 let mut empty_config = String::new();
732
733 bump_epoch_in_conf("mymeasurement", &mut empty_config).unwrap();
735
736 assert!(empty_config.contains("[measurement]"));
738 assert!(empty_config.contains("[measurement.mymeasurement]"));
739 assert!(empty_config.contains("epoch ="));
740 assert!(!empty_config.contains("measurement = {"));
742 assert!(!empty_config.contains("mymeasurement = {"));
743
744 let mut existing_config = r#"[measurement]
746existing_setting = "value"
747
748[measurement."other"]
749epoch = "oldvalue"
750"#
751 .to_string();
752
753 bump_epoch_in_conf("newmeasurement", &mut existing_config).unwrap();
754
755 assert!(existing_config.contains("[measurement.newmeasurement]"));
757 assert!(existing_config.contains("existing_setting = \"value\""));
758 assert!(existing_config.contains("[measurement.\"other\"]"));
759 assert!(!existing_config.contains("newmeasurement = {"));
760 });
761 }
762
763 #[test]
764 fn test_find_config_path_in_git_root() {
765 with_isolated_home(|temp_dir| {
766 env::set_current_dir(temp_dir).unwrap();
768
769 init_repo(temp_dir);
771
772 let config_path = temp_dir.join(".gitperfconfig");
774 fs::write(
775 &config_path,
776 "[measurement.\"test\"]\nepoch = \"12345678\"\n",
777 )
778 .unwrap();
779
780 let found_path = find_config_path();
782 assert!(found_path.is_some());
783 assert_eq!(
785 found_path.unwrap().canonicalize().unwrap(),
786 config_path.canonicalize().unwrap()
787 );
788 });
789 }
790
791 #[test]
792 fn test_find_config_path_not_found() {
793 with_isolated_home(|temp_dir| {
794 env::set_current_dir(temp_dir).unwrap();
796
797 init_repo(temp_dir);
799
800 let found_path = find_config_path();
802 assert!(found_path.is_none());
803 });
804 }
805
806 #[test]
807 fn test_hierarchical_config_workspace_overrides_home() {
808 with_isolated_home(|temp_dir| {
809 env::set_current_dir(temp_dir).unwrap();
811
812 init_repo(temp_dir);
814
815 let home_config_path = create_home_config_dir(temp_dir);
817 fs::write(
818 &home_config_path,
819 r#"
820[measurement."test"]
821backoff_max_elapsed_seconds = 30
822audit_min_relative_deviation = 1.0
823"#,
824 )
825 .unwrap();
826
827 let workspace_config_path = temp_dir.join(".gitperfconfig");
829 fs::write(
830 &workspace_config_path,
831 r#"
832[measurement."test"]
833backoff_max_elapsed_seconds = 60
834"#,
835 )
836 .unwrap();
837
838 env::set_var("HOME", temp_dir);
840 env::remove_var("XDG_CONFIG_HOME");
841
842 let config = read_hierarchical_config().unwrap();
844
845 let backoff: i32 = config
847 .get("measurement.test.backoff_max_elapsed_seconds")
848 .unwrap();
849 assert_eq!(backoff, 60);
850
851 let deviation: f64 = config
853 .get("measurement.test.audit_min_relative_deviation")
854 .unwrap();
855 assert_eq!(deviation, 1.0);
856 });
857 }
858
859 #[test]
860 fn test_determine_epoch_from_config_with_missing_file() {
861 let temp_dir = TempDir::new().unwrap();
863 fs::create_dir_all(temp_dir.path()).unwrap();
864 env::set_current_dir(temp_dir.path()).unwrap();
865
866 let epoch = determine_epoch_from_config("test_measurement");
867 assert!(epoch.is_none());
868 }
869
870 #[test]
871 fn test_determine_epoch_from_config_with_invalid_toml() {
872 let temp_dir = TempDir::new().unwrap();
873 let config_path = temp_dir.path().join(".gitperfconfig");
874 fs::write(&config_path, "invalid toml content").unwrap();
875
876 fs::create_dir_all(temp_dir.path()).unwrap();
877 env::set_current_dir(temp_dir.path()).unwrap();
878
879 let epoch = determine_epoch_from_config("test_measurement");
880 assert!(epoch.is_none());
881 }
882
883 #[test]
884 fn test_write_config_creates_file() {
885 with_isolated_home(|temp_dir| {
886 env::set_current_dir(temp_dir).unwrap();
888 init_repo(temp_dir);
889
890 let subdir = temp_dir.join("a").join("b").join("c");
892 fs::create_dir_all(&subdir).unwrap();
893 env::set_current_dir(&subdir).unwrap();
894
895 let config_content = "[measurement.\"test\"]\nepoch = \"12345678\"\n";
896 write_config(config_content).unwrap();
897
898 let repo_config_path = temp_dir.join(".gitperfconfig");
900 let subdir_config_path = subdir.join(".gitperfconfig");
901
902 assert!(repo_config_path.is_file());
903 assert!(!subdir_config_path.is_file());
904
905 let content = fs::read_to_string(&repo_config_path).unwrap();
906 assert_eq!(content, config_content);
907 });
908 }
909
910 #[test]
911 fn test_hierarchical_config_system_override() {
912 with_isolated_home(|temp_dir| {
913 let system_config_path = create_home_config_dir(temp_dir);
915 let system_config = r#"
916[measurement]
917min_relative_deviation = 5.0
918dispersion_method = "mad"
919
920[backoff]
921max_elapsed_seconds = 120
922"#;
923 fs::write(&system_config_path, system_config).unwrap();
924
925 env::set_current_dir(temp_dir).unwrap();
927 init_repo(temp_dir);
928
929 let workspace_config_path = temp_dir.join(".gitperfconfig");
931 let local_config = r#"
932[measurement]
933min_relative_deviation = 10.0
934
935[measurement."build_time"]
936min_relative_deviation = 15.0
937dispersion_method = "stddev"
938"#;
939 fs::write(&workspace_config_path, local_config).unwrap();
940
941 let config = read_hierarchical_config().unwrap();
943
944 use super::ConfigParentFallbackExt;
946 assert_eq!(
947 config
948 .get_with_parent_fallback(
949 "measurement",
950 "any_measurement",
951 "min_relative_deviation"
952 )
953 .unwrap()
954 .parse::<f64>()
955 .unwrap(),
956 10.0
957 );
958 assert_eq!(
959 config
960 .get_with_parent_fallback("measurement", "any_measurement", "dispersion_method")
961 .unwrap(),
962 "mad"
963 ); assert_eq!(
967 config
968 .get_float("measurement.build_time.min_relative_deviation")
969 .unwrap(),
970 15.0
971 );
972 assert_eq!(
973 config
974 .get_string("measurement.build_time.dispersion_method")
975 .unwrap(),
976 "stddev"
977 );
978
979 assert_eq!(config.get_int("backoff.max_elapsed_seconds").unwrap(), 120);
981
982 assert_eq!(audit_min_relative_deviation("build_time"), Some(15.0));
984 assert_eq!(
985 audit_min_relative_deviation("other_measurement"),
986 Some(10.0)
987 );
988 assert_eq!(
989 audit_dispersion_method("build_time"),
990 git_perf_cli_types::DispersionMethod::StandardDeviation
991 );
992 assert_eq!(
993 audit_dispersion_method("other_measurement"),
994 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
995 );
996 assert_eq!(backoff_max_elapsed_seconds(), 120);
997 });
998 }
999
1000 #[test]
1001 fn test_read_config_from_file_missing_file() {
1002 let temp_dir = TempDir::new().unwrap();
1003 let nonexistent_file = temp_dir.path().join("does_not_exist.toml");
1004
1005 let result = read_config_from_file(&nonexistent_file);
1007 assert!(result.is_err());
1008 }
1009
1010 #[test]
1011 fn test_read_config_from_file_valid_content() {
1012 let temp_dir = TempDir::new().unwrap();
1013 let config_file = temp_dir.path().join("test_config.toml");
1014 let expected_content = "[measurement]\nepoch = \"12345678\"\n";
1015
1016 fs::write(&config_file, expected_content).unwrap();
1017
1018 let result = read_config_from_file(&config_file);
1019 assert!(result.is_ok());
1020 let content = result.unwrap();
1021 assert_eq!(content, expected_content);
1022
1023 assert!(!content.is_empty());
1025 }
1026
1027 #[test]
1028 fn test_audit_min_measurements() {
1029 with_isolated_home(|temp_dir| {
1030 env::set_current_dir(temp_dir).unwrap();
1032 init_repo(temp_dir);
1033
1034 let workspace_config_path = temp_dir.join(".gitperfconfig");
1036 let local_config = r#"
1037[measurement]
1038min_measurements = 5
1039
1040[measurement."build_time"]
1041min_measurements = 10
1042
1043[measurement."memory_usage"]
1044min_measurements = 3
1045"#;
1046 fs::write(&workspace_config_path, local_config).unwrap();
1047
1048 assert_eq!(super::audit_min_measurements("build_time"), Some(10));
1050 assert_eq!(super::audit_min_measurements("memory_usage"), Some(3));
1051 assert_eq!(super::audit_min_measurements("other_measurement"), Some(5));
1052
1053 fs::remove_file(&workspace_config_path).unwrap();
1055 assert_eq!(super::audit_min_measurements("any_measurement"), None);
1056 });
1057 }
1058
1059 #[test]
1060 fn test_audit_aggregate_by() {
1061 with_isolated_home(|temp_dir| {
1062 env::set_current_dir(temp_dir).unwrap();
1064 init_repo(temp_dir);
1065
1066 let workspace_config_path = temp_dir.join(".gitperfconfig");
1068 let local_config = r#"
1069[measurement]
1070aggregate_by = "median"
1071
1072[measurement."build_time"]
1073aggregate_by = "max"
1074
1075[measurement."memory_usage"]
1076aggregate_by = "mean"
1077"#;
1078 fs::write(&workspace_config_path, local_config).unwrap();
1079
1080 assert_eq!(
1082 super::audit_aggregate_by("build_time"),
1083 Some(git_perf_cli_types::ReductionFunc::Max)
1084 );
1085 assert_eq!(
1086 super::audit_aggregate_by("memory_usage"),
1087 Some(git_perf_cli_types::ReductionFunc::Mean)
1088 );
1089 assert_eq!(
1090 super::audit_aggregate_by("other_measurement"),
1091 Some(git_perf_cli_types::ReductionFunc::Median)
1092 );
1093
1094 fs::remove_file(&workspace_config_path).unwrap();
1096 assert_eq!(super::audit_aggregate_by("any_measurement"), None);
1097 });
1098 }
1099
1100 #[test]
1101 fn test_audit_sigma() {
1102 with_isolated_home(|temp_dir| {
1103 env::set_current_dir(temp_dir).unwrap();
1105 init_repo(temp_dir);
1106
1107 let workspace_config_path = temp_dir.join(".gitperfconfig");
1109 let local_config = r#"
1110[measurement]
1111sigma = 3.0
1112
1113[measurement."build_time"]
1114sigma = 5.5
1115
1116[measurement."memory_usage"]
1117sigma = 2.0
1118"#;
1119 fs::write(&workspace_config_path, local_config).unwrap();
1120
1121 assert_eq!(super::audit_sigma("build_time"), Some(5.5));
1123 assert_eq!(super::audit_sigma("memory_usage"), Some(2.0));
1124 assert_eq!(super::audit_sigma("other_measurement"), Some(3.0));
1125
1126 fs::remove_file(&workspace_config_path).unwrap();
1128 assert_eq!(super::audit_sigma("any_measurement"), None);
1129 });
1130 }
1131
1132 #[test]
1133 fn test_measurement_unit() {
1134 with_isolated_home(|temp_dir| {
1135 env::set_current_dir(temp_dir).unwrap();
1137 init_repo(temp_dir);
1138
1139 let workspace_config_path = temp_dir.join(".gitperfconfig");
1141 let local_config = r#"
1142[measurement]
1143unit = "ms"
1144
1145[measurement."build_time"]
1146unit = "ms"
1147
1148[measurement."memory_usage"]
1149unit = "bytes"
1150
1151[measurement."throughput"]
1152unit = "requests/sec"
1153"#;
1154 fs::write(&workspace_config_path, local_config).unwrap();
1155
1156 assert_eq!(
1158 super::measurement_unit("build_time"),
1159 Some("ms".to_string())
1160 );
1161 assert_eq!(
1162 super::measurement_unit("memory_usage"),
1163 Some("bytes".to_string())
1164 );
1165 assert_eq!(
1166 super::measurement_unit("throughput"),
1167 Some("requests/sec".to_string())
1168 );
1169
1170 assert_eq!(
1172 super::measurement_unit("other_measurement"),
1173 Some("ms".to_string())
1174 );
1175
1176 fs::remove_file(&workspace_config_path).unwrap();
1178 assert_eq!(super::measurement_unit("any_measurement"), None);
1179 });
1180 }
1181
1182 #[test]
1183 fn test_measurement_unit_precedence() {
1184 with_isolated_home(|temp_dir| {
1185 env::set_current_dir(temp_dir).unwrap();
1187 init_repo(temp_dir);
1188
1189 let workspace_config_path = temp_dir.join(".gitperfconfig");
1191 let precedence_config = r#"
1192[measurement]
1193unit = "ms"
1194
1195[measurement."build_time"]
1196unit = "seconds"
1197"#;
1198 fs::write(&workspace_config_path, precedence_config).unwrap();
1199
1200 assert_eq!(
1202 super::measurement_unit("build_time"),
1203 Some("seconds".to_string())
1204 );
1205
1206 assert_eq!(
1208 super::measurement_unit("other_measurement"),
1209 Some("ms".to_string())
1210 );
1211 });
1212 }
1213
1214 #[test]
1215 fn test_measurement_unit_no_parent_default() {
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."build_time"]
1225unit = "ms"
1226
1227[measurement."memory_usage"]
1228unit = "bytes"
1229"#;
1230 fs::write(&workspace_config_path, local_config).unwrap();
1231
1232 assert_eq!(
1234 super::measurement_unit("build_time"),
1235 Some("ms".to_string())
1236 );
1237 assert_eq!(
1238 super::measurement_unit("memory_usage"),
1239 Some("bytes".to_string())
1240 );
1241
1242 assert_eq!(super::measurement_unit("other_measurement"), None);
1244 });
1245 }
1246}