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_dispersion_method(measurement: &str) -> DispersionMethod {
210 let Some(config) = read_hierarchical_config().ok() else {
211 return DispersionMethod::StandardDeviation;
212 };
213
214 if let Some(s) =
215 config.get_with_parent_fallback("measurement", measurement, "dispersion_method")
216 {
217 if let Ok(method) = s.parse::<DispersionMethod>() {
218 return method;
219 }
220 }
221
222 DispersionMethod::StandardDeviation
223}
224
225#[must_use]
227pub fn audit_min_measurements(measurement: &str) -> Option<u16> {
228 let config = read_hierarchical_config().ok()?;
229
230 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "min_measurements")
231 {
232 if let Ok(v) = s.parse::<u16>() {
233 return Some(v);
234 }
235 }
236
237 None
238}
239
240#[must_use]
242pub fn audit_aggregate_by(measurement: &str) -> Option<git_perf_cli_types::ReductionFunc> {
243 let config = read_hierarchical_config().ok()?;
244
245 let s = config.get_with_parent_fallback("measurement", measurement, "aggregate_by")?;
246
247 match s.to_lowercase().as_str() {
249 "min" => Some(git_perf_cli_types::ReductionFunc::Min),
250 "max" => Some(git_perf_cli_types::ReductionFunc::Max),
251 "median" => Some(git_perf_cli_types::ReductionFunc::Median),
252 "mean" => Some(git_perf_cli_types::ReductionFunc::Mean),
253 _ => None,
254 }
255}
256
257#[must_use]
259pub fn audit_sigma(measurement: &str) -> Option<f64> {
260 let config = read_hierarchical_config().ok()?;
261
262 if let Some(s) = config.get_with_parent_fallback("measurement", measurement, "sigma") {
263 if let Ok(v) = s.parse::<f64>() {
264 return Some(v);
265 }
266 }
267
268 None
269}
270
271#[must_use]
273pub fn measurement_unit(measurement: &str) -> Option<String> {
274 let config = read_hierarchical_config().ok()?;
275 config.get_with_parent_fallback("measurement", measurement, "unit")
276}
277
278#[must_use]
280pub fn report_template_path() -> Option<PathBuf> {
281 let config = read_hierarchical_config().ok()?;
282 let path_str = config.get_string("report.template_path").ok()?;
283 Some(PathBuf::from(path_str))
284}
285
286#[must_use]
288pub fn report_custom_css_path() -> Option<PathBuf> {
289 let config = read_hierarchical_config().ok()?;
290 let path_str = config.get_string("report.custom_css_path").ok()?;
291 Some(PathBuf::from(path_str))
292}
293
294#[must_use]
296pub fn report_title() -> Option<String> {
297 let config = read_hierarchical_config().ok()?;
298 config.get_string("report.title").ok()
299}
300
301#[must_use]
309pub fn change_point_config(measurement: &str) -> crate::change_point::ChangePointConfig {
310 let mut config = crate::change_point::ChangePointConfig::default();
311
312 let Ok(file_config) = read_hierarchical_config() else {
313 return config;
314 };
315
316 if let Some(enabled_str) =
318 file_config.get_with_parent_fallback("change_point", measurement, "enabled")
319 {
320 if let Ok(enabled) = enabled_str.parse::<bool>() {
321 if !enabled {
322 config.min_data_points = usize::MAX;
324 return config;
325 }
326 }
327 }
328
329 if let Some(s) =
331 file_config.get_with_parent_fallback("change_point", measurement, "min_data_points")
332 {
333 if let Ok(v) = s.parse::<usize>() {
334 config.min_data_points = v;
335 }
336 }
337
338 if let Some(s) =
340 file_config.get_with_parent_fallback("change_point", measurement, "min_magnitude_pct")
341 {
342 if let Ok(v) = s.parse::<f64>() {
343 config.min_magnitude_pct = v;
344 }
345 }
346
347 if let Some(s) = file_config.get_with_parent_fallback("change_point", measurement, "penalty") {
349 if let Ok(v) = s.parse::<f64>() {
350 config.penalty = v;
351 }
352 }
353
354 config
355}
356
357#[cfg(test)]
358mod test {
359 use super::*;
360 use crate::test_helpers::{
361 hermetic_git_env, init_repo, init_repo_with_file, with_isolated_home,
362 };
363 use std::fs;
364 use tempfile::TempDir;
365
366 fn create_home_config_dir(home_dir: &Path) -> PathBuf {
368 let config_dir = home_dir.join(".config").join("git-perf");
369 fs::create_dir_all(&config_dir).unwrap();
370 config_dir.join("config.toml")
371 }
372
373 #[test]
374 fn test_read_epochs() {
375 with_isolated_home(|temp_dir| {
376 env::set_current_dir(temp_dir).unwrap();
378 init_repo(temp_dir);
379
380 let workspace_config_path = temp_dir.join(".gitperfconfig");
382 let configfile = r#"[measurement]
383# General performance regression
384epoch="12344555"
385
386[measurement."something"]
387#My comment
388epoch="34567898"
389
390[measurement."somethingelse"]
391epoch="a3dead"
392"#;
393 fs::write(&workspace_config_path, configfile).unwrap();
394
395 let epoch = determine_epoch_from_config("something");
396 assert_eq!(epoch, Some(0x34567898));
397
398 let epoch = determine_epoch_from_config("somethingelse");
399 assert_eq!(epoch, Some(0xa3dead));
400
401 let epoch = determine_epoch_from_config("unspecified");
402 assert_eq!(epoch, Some(0x12344555));
403 });
404 }
405
406 #[test]
407 fn test_bump_epochs() {
408 with_isolated_home(|temp_dir| {
409 env::set_current_dir(temp_dir).unwrap();
411
412 hermetic_git_env();
414
415 init_repo_with_file(temp_dir);
417
418 let configfile = r#"[measurement."something"]
419#My comment
420epoch = "34567898"
421"#;
422
423 let mut actual = String::from(configfile);
424 bump_epoch_in_conf("something", &mut actual).expect("Failed to bump epoch");
425
426 let expected = format!(
427 r#"[measurement."something"]
428#My comment
429epoch = "{}"
430"#,
431 &get_head_revision().expect("get_head_revision failed")[0..8],
432 );
433
434 assert_eq!(actual, expected);
435 });
436 }
437
438 #[test]
439 fn test_bump_new_epoch_and_read_it() {
440 with_isolated_home(|temp_dir| {
441 env::set_current_dir(temp_dir).unwrap();
443
444 hermetic_git_env();
446
447 init_repo_with_file(temp_dir);
449
450 let mut conf = String::new();
451 bump_epoch_in_conf("mymeasurement", &mut conf).expect("Failed to bump epoch");
452
453 let config_path = temp_dir.join(".gitperfconfig");
455 fs::write(&config_path, &conf).unwrap();
456
457 let epoch = determine_epoch_from_config("mymeasurement");
458 assert!(epoch.is_some());
459 });
460 }
461
462 #[test]
463 fn test_backoff_max_elapsed_seconds() {
464 with_isolated_home(|temp_dir| {
465 env::set_current_dir(temp_dir).unwrap();
467 init_repo(temp_dir);
468
469 let workspace_config_path = temp_dir.join(".gitperfconfig");
471 let local_config = "[backoff]\nmax_elapsed_seconds = 42\n";
472 fs::write(&workspace_config_path, local_config).unwrap();
473
474 assert_eq!(super::backoff_max_elapsed_seconds(), 42);
476
477 fs::remove_file(&workspace_config_path).unwrap();
479 assert_eq!(super::backoff_max_elapsed_seconds(), 60);
480 });
481 }
482
483 #[test]
484 fn test_audit_min_relative_deviation() {
485 with_isolated_home(|temp_dir| {
486 env::set_current_dir(temp_dir).unwrap();
488 init_repo(temp_dir);
489
490 let workspace_config_path = temp_dir.join(".gitperfconfig");
492 let local_config = r#"
493[measurement]
494min_relative_deviation = 5.0
495
496[measurement."build_time"]
497min_relative_deviation = 10.0
498
499[measurement."memory_usage"]
500min_relative_deviation = 2.5
501"#;
502 fs::write(&workspace_config_path, local_config).unwrap();
503
504 assert_eq!(
506 super::audit_min_relative_deviation("build_time"),
507 Some(10.0)
508 );
509 assert_eq!(
510 super::audit_min_relative_deviation("memory_usage"),
511 Some(2.5)
512 );
513 assert_eq!(
514 super::audit_min_relative_deviation("other_measurement"),
515 Some(5.0) );
517
518 let global_config = r#"
520[measurement]
521min_relative_deviation = 5.0
522"#;
523 fs::write(&workspace_config_path, global_config).unwrap();
524 assert_eq!(
525 super::audit_min_relative_deviation("any_measurement"),
526 Some(5.0)
527 );
528
529 let precedence_config = r#"
531[measurement]
532min_relative_deviation = 5.0
533
534[measurement."build_time"]
535min_relative_deviation = 10.0
536"#;
537 fs::write(&workspace_config_path, precedence_config).unwrap();
538 assert_eq!(
539 super::audit_min_relative_deviation("build_time"),
540 Some(10.0)
541 );
542 assert_eq!(
543 super::audit_min_relative_deviation("other_measurement"),
544 Some(5.0)
545 );
546
547 fs::remove_file(&workspace_config_path).unwrap();
549 assert_eq!(super::audit_min_relative_deviation("any_measurement"), None);
550 });
551 }
552
553 #[test]
554 fn test_audit_dispersion_method() {
555 with_isolated_home(|temp_dir| {
556 env::set_current_dir(temp_dir).unwrap();
558 init_repo(temp_dir);
559
560 let workspace_config_path = temp_dir.join(".gitperfconfig");
562 let local_config = r#"
563[measurement]
564dispersion_method = "stddev"
565
566[measurement."build_time"]
567dispersion_method = "mad"
568
569[measurement."memory_usage"]
570dispersion_method = "stddev"
571"#;
572 fs::write(&workspace_config_path, local_config).unwrap();
573
574 assert_eq!(
576 super::audit_dispersion_method("build_time"),
577 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
578 );
579 assert_eq!(
580 super::audit_dispersion_method("memory_usage"),
581 git_perf_cli_types::DispersionMethod::StandardDeviation
582 );
583 assert_eq!(
584 super::audit_dispersion_method("other_measurement"),
585 git_perf_cli_types::DispersionMethod::StandardDeviation
586 );
587
588 let global_config = r#"
590[measurement]
591dispersion_method = "mad"
592"#;
593 fs::write(&workspace_config_path, global_config).unwrap();
594 assert_eq!(
595 super::audit_dispersion_method("any_measurement"),
596 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
597 );
598
599 let precedence_config = r#"
601[measurement]
602dispersion_method = "mad"
603
604[measurement."build_time"]
605dispersion_method = "stddev"
606"#;
607 fs::write(&workspace_config_path, precedence_config).unwrap();
608 assert_eq!(
609 super::audit_dispersion_method("build_time"),
610 git_perf_cli_types::DispersionMethod::StandardDeviation
611 );
612 assert_eq!(
613 super::audit_dispersion_method("other_measurement"),
614 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
615 );
616
617 fs::remove_file(&workspace_config_path).unwrap();
619 assert_eq!(
620 super::audit_dispersion_method("any_measurement"),
621 git_perf_cli_types::DispersionMethod::StandardDeviation
622 );
623 });
624 }
625
626 #[test]
627 fn test_bump_epoch_in_conf_creates_proper_tables() {
628 with_isolated_home(|temp_dir| {
631 env::set_current_dir(temp_dir).unwrap();
632
633 hermetic_git_env();
635
636 init_repo_with_file(temp_dir);
637
638 let mut empty_config = String::new();
640
641 bump_epoch_in_conf("mymeasurement", &mut empty_config).unwrap();
643
644 assert!(empty_config.contains("[measurement]"));
646 assert!(empty_config.contains("[measurement.mymeasurement]"));
647 assert!(empty_config.contains("epoch ="));
648 assert!(!empty_config.contains("measurement = {"));
650 assert!(!empty_config.contains("mymeasurement = {"));
651
652 let mut existing_config = r#"[measurement]
654existing_setting = "value"
655
656[measurement."other"]
657epoch = "oldvalue"
658"#
659 .to_string();
660
661 bump_epoch_in_conf("newmeasurement", &mut existing_config).unwrap();
662
663 assert!(existing_config.contains("[measurement.newmeasurement]"));
665 assert!(existing_config.contains("existing_setting = \"value\""));
666 assert!(existing_config.contains("[measurement.\"other\"]"));
667 assert!(!existing_config.contains("newmeasurement = {"));
668 });
669 }
670
671 #[test]
672 fn test_find_config_path_in_git_root() {
673 with_isolated_home(|temp_dir| {
674 env::set_current_dir(temp_dir).unwrap();
676
677 init_repo(temp_dir);
679
680 let config_path = temp_dir.join(".gitperfconfig");
682 fs::write(
683 &config_path,
684 "[measurement.\"test\"]\nepoch = \"12345678\"\n",
685 )
686 .unwrap();
687
688 let found_path = find_config_path();
690 assert!(found_path.is_some());
691 assert_eq!(
693 found_path.unwrap().canonicalize().unwrap(),
694 config_path.canonicalize().unwrap()
695 );
696 });
697 }
698
699 #[test]
700 fn test_find_config_path_not_found() {
701 with_isolated_home(|temp_dir| {
702 env::set_current_dir(temp_dir).unwrap();
704
705 init_repo(temp_dir);
707
708 let found_path = find_config_path();
710 assert!(found_path.is_none());
711 });
712 }
713
714 #[test]
715 fn test_hierarchical_config_workspace_overrides_home() {
716 with_isolated_home(|temp_dir| {
717 env::set_current_dir(temp_dir).unwrap();
719
720 init_repo(temp_dir);
722
723 let home_config_path = create_home_config_dir(temp_dir);
725 fs::write(
726 &home_config_path,
727 r#"
728[measurement."test"]
729backoff_max_elapsed_seconds = 30
730audit_min_relative_deviation = 1.0
731"#,
732 )
733 .unwrap();
734
735 let workspace_config_path = temp_dir.join(".gitperfconfig");
737 fs::write(
738 &workspace_config_path,
739 r#"
740[measurement."test"]
741backoff_max_elapsed_seconds = 60
742"#,
743 )
744 .unwrap();
745
746 env::set_var("HOME", temp_dir);
748 env::remove_var("XDG_CONFIG_HOME");
749
750 let config = read_hierarchical_config().unwrap();
752
753 let backoff: i32 = config
755 .get("measurement.test.backoff_max_elapsed_seconds")
756 .unwrap();
757 assert_eq!(backoff, 60);
758
759 let deviation: f64 = config
761 .get("measurement.test.audit_min_relative_deviation")
762 .unwrap();
763 assert_eq!(deviation, 1.0);
764 });
765 }
766
767 #[test]
768 fn test_determine_epoch_from_config_with_missing_file() {
769 let temp_dir = TempDir::new().unwrap();
771 fs::create_dir_all(temp_dir.path()).unwrap();
772 env::set_current_dir(temp_dir.path()).unwrap();
773
774 let epoch = determine_epoch_from_config("test_measurement");
775 assert!(epoch.is_none());
776 }
777
778 #[test]
779 fn test_determine_epoch_from_config_with_invalid_toml() {
780 let temp_dir = TempDir::new().unwrap();
781 let config_path = temp_dir.path().join(".gitperfconfig");
782 fs::write(&config_path, "invalid toml content").unwrap();
783
784 fs::create_dir_all(temp_dir.path()).unwrap();
785 env::set_current_dir(temp_dir.path()).unwrap();
786
787 let epoch = determine_epoch_from_config("test_measurement");
788 assert!(epoch.is_none());
789 }
790
791 #[test]
792 fn test_write_config_creates_file() {
793 with_isolated_home(|temp_dir| {
794 env::set_current_dir(temp_dir).unwrap();
796 init_repo(temp_dir);
797
798 let subdir = temp_dir.join("a").join("b").join("c");
800 fs::create_dir_all(&subdir).unwrap();
801 env::set_current_dir(&subdir).unwrap();
802
803 let config_content = "[measurement.\"test\"]\nepoch = \"12345678\"\n";
804 write_config(config_content).unwrap();
805
806 let repo_config_path = temp_dir.join(".gitperfconfig");
808 let subdir_config_path = subdir.join(".gitperfconfig");
809
810 assert!(repo_config_path.is_file());
811 assert!(!subdir_config_path.is_file());
812
813 let content = fs::read_to_string(&repo_config_path).unwrap();
814 assert_eq!(content, config_content);
815 });
816 }
817
818 #[test]
819 fn test_hierarchical_config_system_override() {
820 with_isolated_home(|temp_dir| {
821 let system_config_path = create_home_config_dir(temp_dir);
823 let system_config = r#"
824[measurement]
825min_relative_deviation = 5.0
826dispersion_method = "mad"
827
828[backoff]
829max_elapsed_seconds = 120
830"#;
831 fs::write(&system_config_path, system_config).unwrap();
832
833 env::set_current_dir(temp_dir).unwrap();
835 init_repo(temp_dir);
836
837 let workspace_config_path = temp_dir.join(".gitperfconfig");
839 let local_config = r#"
840[measurement]
841min_relative_deviation = 10.0
842
843[measurement."build_time"]
844min_relative_deviation = 15.0
845dispersion_method = "stddev"
846"#;
847 fs::write(&workspace_config_path, local_config).unwrap();
848
849 let config = read_hierarchical_config().unwrap();
851
852 use super::ConfigParentFallbackExt;
854 assert_eq!(
855 config
856 .get_with_parent_fallback(
857 "measurement",
858 "any_measurement",
859 "min_relative_deviation"
860 )
861 .unwrap()
862 .parse::<f64>()
863 .unwrap(),
864 10.0
865 );
866 assert_eq!(
867 config
868 .get_with_parent_fallback("measurement", "any_measurement", "dispersion_method")
869 .unwrap(),
870 "mad"
871 ); assert_eq!(
875 config
876 .get_float("measurement.build_time.min_relative_deviation")
877 .unwrap(),
878 15.0
879 );
880 assert_eq!(
881 config
882 .get_string("measurement.build_time.dispersion_method")
883 .unwrap(),
884 "stddev"
885 );
886
887 assert_eq!(config.get_int("backoff.max_elapsed_seconds").unwrap(), 120);
889
890 assert_eq!(audit_min_relative_deviation("build_time"), Some(15.0));
892 assert_eq!(
893 audit_min_relative_deviation("other_measurement"),
894 Some(10.0)
895 );
896 assert_eq!(
897 audit_dispersion_method("build_time"),
898 git_perf_cli_types::DispersionMethod::StandardDeviation
899 );
900 assert_eq!(
901 audit_dispersion_method("other_measurement"),
902 git_perf_cli_types::DispersionMethod::MedianAbsoluteDeviation
903 );
904 assert_eq!(backoff_max_elapsed_seconds(), 120);
905 });
906 }
907
908 #[test]
909 fn test_read_config_from_file_missing_file() {
910 let temp_dir = TempDir::new().unwrap();
911 let nonexistent_file = temp_dir.path().join("does_not_exist.toml");
912
913 let result = read_config_from_file(&nonexistent_file);
915 assert!(result.is_err());
916 }
917
918 #[test]
919 fn test_read_config_from_file_valid_content() {
920 let temp_dir = TempDir::new().unwrap();
921 let config_file = temp_dir.path().join("test_config.toml");
922 let expected_content = "[measurement]\nepoch = \"12345678\"\n";
923
924 fs::write(&config_file, expected_content).unwrap();
925
926 let result = read_config_from_file(&config_file);
927 assert!(result.is_ok());
928 let content = result.unwrap();
929 assert_eq!(content, expected_content);
930
931 assert!(!content.is_empty());
933 }
934
935 #[test]
936 fn test_audit_min_measurements() {
937 with_isolated_home(|temp_dir| {
938 env::set_current_dir(temp_dir).unwrap();
940 init_repo(temp_dir);
941
942 let workspace_config_path = temp_dir.join(".gitperfconfig");
944 let local_config = r#"
945[measurement]
946min_measurements = 5
947
948[measurement."build_time"]
949min_measurements = 10
950
951[measurement."memory_usage"]
952min_measurements = 3
953"#;
954 fs::write(&workspace_config_path, local_config).unwrap();
955
956 assert_eq!(super::audit_min_measurements("build_time"), Some(10));
958 assert_eq!(super::audit_min_measurements("memory_usage"), Some(3));
959 assert_eq!(super::audit_min_measurements("other_measurement"), Some(5));
960
961 fs::remove_file(&workspace_config_path).unwrap();
963 assert_eq!(super::audit_min_measurements("any_measurement"), None);
964 });
965 }
966
967 #[test]
968 fn test_audit_aggregate_by() {
969 with_isolated_home(|temp_dir| {
970 env::set_current_dir(temp_dir).unwrap();
972 init_repo(temp_dir);
973
974 let workspace_config_path = temp_dir.join(".gitperfconfig");
976 let local_config = r#"
977[measurement]
978aggregate_by = "median"
979
980[measurement."build_time"]
981aggregate_by = "max"
982
983[measurement."memory_usage"]
984aggregate_by = "mean"
985"#;
986 fs::write(&workspace_config_path, local_config).unwrap();
987
988 assert_eq!(
990 super::audit_aggregate_by("build_time"),
991 Some(git_perf_cli_types::ReductionFunc::Max)
992 );
993 assert_eq!(
994 super::audit_aggregate_by("memory_usage"),
995 Some(git_perf_cli_types::ReductionFunc::Mean)
996 );
997 assert_eq!(
998 super::audit_aggregate_by("other_measurement"),
999 Some(git_perf_cli_types::ReductionFunc::Median)
1000 );
1001
1002 fs::remove_file(&workspace_config_path).unwrap();
1004 assert_eq!(super::audit_aggregate_by("any_measurement"), None);
1005 });
1006 }
1007
1008 #[test]
1009 fn test_audit_sigma() {
1010 with_isolated_home(|temp_dir| {
1011 env::set_current_dir(temp_dir).unwrap();
1013 init_repo(temp_dir);
1014
1015 let workspace_config_path = temp_dir.join(".gitperfconfig");
1017 let local_config = r#"
1018[measurement]
1019sigma = 3.0
1020
1021[measurement."build_time"]
1022sigma = 5.5
1023
1024[measurement."memory_usage"]
1025sigma = 2.0
1026"#;
1027 fs::write(&workspace_config_path, local_config).unwrap();
1028
1029 assert_eq!(super::audit_sigma("build_time"), Some(5.5));
1031 assert_eq!(super::audit_sigma("memory_usage"), Some(2.0));
1032 assert_eq!(super::audit_sigma("other_measurement"), Some(3.0));
1033
1034 fs::remove_file(&workspace_config_path).unwrap();
1036 assert_eq!(super::audit_sigma("any_measurement"), None);
1037 });
1038 }
1039
1040 #[test]
1041 fn test_measurement_unit() {
1042 with_isolated_home(|temp_dir| {
1043 env::set_current_dir(temp_dir).unwrap();
1045 init_repo(temp_dir);
1046
1047 let workspace_config_path = temp_dir.join(".gitperfconfig");
1049 let local_config = r#"
1050[measurement]
1051unit = "ms"
1052
1053[measurement."build_time"]
1054unit = "ms"
1055
1056[measurement."memory_usage"]
1057unit = "bytes"
1058
1059[measurement."throughput"]
1060unit = "requests/sec"
1061"#;
1062 fs::write(&workspace_config_path, local_config).unwrap();
1063
1064 assert_eq!(
1066 super::measurement_unit("build_time"),
1067 Some("ms".to_string())
1068 );
1069 assert_eq!(
1070 super::measurement_unit("memory_usage"),
1071 Some("bytes".to_string())
1072 );
1073 assert_eq!(
1074 super::measurement_unit("throughput"),
1075 Some("requests/sec".to_string())
1076 );
1077
1078 assert_eq!(
1080 super::measurement_unit("other_measurement"),
1081 Some("ms".to_string())
1082 );
1083
1084 fs::remove_file(&workspace_config_path).unwrap();
1086 assert_eq!(super::measurement_unit("any_measurement"), None);
1087 });
1088 }
1089
1090 #[test]
1091 fn test_measurement_unit_precedence() {
1092 with_isolated_home(|temp_dir| {
1093 env::set_current_dir(temp_dir).unwrap();
1095 init_repo(temp_dir);
1096
1097 let workspace_config_path = temp_dir.join(".gitperfconfig");
1099 let precedence_config = r#"
1100[measurement]
1101unit = "ms"
1102
1103[measurement."build_time"]
1104unit = "seconds"
1105"#;
1106 fs::write(&workspace_config_path, precedence_config).unwrap();
1107
1108 assert_eq!(
1110 super::measurement_unit("build_time"),
1111 Some("seconds".to_string())
1112 );
1113
1114 assert_eq!(
1116 super::measurement_unit("other_measurement"),
1117 Some("ms".to_string())
1118 );
1119 });
1120 }
1121
1122 #[test]
1123 fn test_measurement_unit_no_parent_default() {
1124 with_isolated_home(|temp_dir| {
1125 env::set_current_dir(temp_dir).unwrap();
1127 init_repo(temp_dir);
1128
1129 let workspace_config_path = temp_dir.join(".gitperfconfig");
1131 let local_config = r#"
1132[measurement."build_time"]
1133unit = "ms"
1134
1135[measurement."memory_usage"]
1136unit = "bytes"
1137"#;
1138 fs::write(&workspace_config_path, local_config).unwrap();
1139
1140 assert_eq!(
1142 super::measurement_unit("build_time"),
1143 Some("ms".to_string())
1144 );
1145 assert_eq!(
1146 super::measurement_unit("memory_usage"),
1147 Some("bytes".to_string())
1148 );
1149
1150 assert_eq!(super::measurement_unit("other_measurement"), None);
1152 });
1153 }
1154}