1use crate::config::{
2 audit_aggregate_by, audit_dispersion_method, audit_min_measurements,
3 audit_min_relative_deviation, audit_sigma, backoff_max_elapsed_seconds,
4 determine_epoch_from_config, measurement_unit, read_hierarchical_config,
5};
6use crate::git::git_interop::get_repository_root;
7use anyhow::{Context, Result};
8use config::Config;
9use git_perf_cli_types::ConfigFormat;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::PathBuf;
13
14#[derive(Debug, Serialize, Deserialize)]
16pub struct ConfigInfo {
17 pub git_context: GitContext,
19
20 pub config_sources: ConfigSources,
22
23 pub global_settings: GlobalSettings,
25
26 pub measurements: HashMap<String, MeasurementConfig>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub validation_issues: Option<Vec<String>>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
36pub struct GitContext {
37 pub branch: String,
39
40 pub repository_root: PathBuf,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
46pub struct ConfigSources {
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub system_config: Option<PathBuf>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub local_config: Option<PathBuf>,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
58pub struct GlobalSettings {
59 pub backoff_max_elapsed_seconds: u64,
61}
62
63#[derive(Debug, Serialize, Deserialize)]
65pub struct MeasurementConfig {
66 pub name: String,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub epoch: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub min_relative_deviation: Option<f64>,
76
77 pub dispersion_method: String,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub min_measurements: Option<u16>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub aggregate_by: Option<String>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub sigma: Option<f64>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub unit: Option<String>,
95
96 pub from_parent_fallback: bool,
98}
99
100pub fn list_config(
102 detailed: bool,
103 format: ConfigFormat,
104 validate: bool,
105 measurement_filter: Option<String>,
106) -> Result<()> {
107 let config_info = gather_config_info(validate, measurement_filter.as_deref())?;
109
110 match format {
112 ConfigFormat::Human => display_human_readable(&config_info, detailed)?,
113 ConfigFormat::Json => display_json(&config_info)?,
114 }
115
116 if validate {
118 if let Some(ref issues) = config_info.validation_issues {
119 if !issues.is_empty() {
120 return Err(anyhow::anyhow!(
121 "Configuration validation found {} issue(s)",
122 issues.len()
123 ));
124 }
125 }
126 }
127
128 Ok(())
129}
130
131fn gather_config_info(validate: bool, measurement_filter: Option<&str>) -> Result<ConfigInfo> {
133 let git_context = gather_git_context()?;
134 let config_sources = gather_config_sources()?;
135 let global_settings = gather_global_settings();
136 let measurements = gather_measurement_configs(measurement_filter)?;
137
138 let validation_issues = if validate {
139 Some(validate_config(&measurements)?)
140 } else {
141 None
142 };
143
144 Ok(ConfigInfo {
145 git_context,
146 config_sources,
147 global_settings,
148 measurements,
149 validation_issues,
150 })
151}
152
153fn gather_git_context() -> Result<GitContext> {
155 let branch_output = std::process::Command::new("git")
157 .args(["rev-parse", "--abbrev-ref", "HEAD"])
158 .output()
159 .context("Failed to get current branch")?;
160
161 let branch = String::from_utf8_lossy(&branch_output.stdout)
162 .trim()
163 .to_string();
164
165 let repo_root = get_repository_root()
167 .map_err(|e| anyhow::anyhow!("Failed to get repository root: {}", e))?;
168 let repository_root = PathBuf::from(repo_root);
169
170 Ok(GitContext {
171 branch,
172 repository_root,
173 })
174}
175
176fn gather_config_sources() -> Result<ConfigSources> {
178 let system_config = find_system_config();
180
181 let local_config = get_local_config_path();
183
184 Ok(ConfigSources {
185 system_config,
186 local_config,
187 })
188}
189
190fn find_system_config() -> Option<PathBuf> {
192 use std::env;
193
194 if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
195 let path = PathBuf::from(xdg_config_home)
196 .join("git-perf")
197 .join("config.toml");
198 if path.exists() {
199 return Some(path);
200 }
201 }
202
203 if let Some(home) = dirs_next::home_dir() {
204 let path = home.join(".config").join("git-perf").join("config.toml");
205 if path.exists() {
206 return Some(path);
207 }
208 }
209
210 None
211}
212
213fn get_local_config_path() -> Option<PathBuf> {
215 let repo_root = get_repository_root().ok()?;
216 let path = PathBuf::from(repo_root).join(".gitperfconfig");
217 if path.exists() {
218 Some(path)
219 } else {
220 None
221 }
222}
223
224fn gather_global_settings() -> GlobalSettings {
226 GlobalSettings {
227 backoff_max_elapsed_seconds: backoff_max_elapsed_seconds(),
228 }
229}
230
231fn gather_measurement_configs(
233 measurement_filter: Option<&str>,
234) -> Result<HashMap<String, MeasurementConfig>> {
235 let mut measurements = HashMap::new();
236
237 let config = match read_hierarchical_config() {
239 Ok(c) => c,
240 Err(_) => {
241 return Ok(measurements);
243 }
244 };
245
246 let measurement_names = extract_measurement_names(&config)?;
248
249 let filtered_names: Vec<String> = if let Some(filter) = measurement_filter {
251 measurement_names
252 .into_iter()
253 .filter(|name| name == filter)
254 .collect()
255 } else {
256 measurement_names
257 };
258
259 for name in filtered_names {
261 let measurement_config = gather_single_measurement_config(&name, &config);
262 measurements.insert(name.clone(), measurement_config);
263 }
264
265 Ok(measurements)
266}
267
268fn extract_measurement_names(config: &Config) -> Result<Vec<String>> {
270 let mut names = Vec::new();
271
272 if let Ok(table) = config.get_table("measurement") {
274 for (key, value) in table {
275 if matches!(value.kind, config::ValueKind::Table(_)) {
277 names.push(key);
278 }
279 }
280 }
281
282 Ok(names)
283}
284
285fn gather_single_measurement_config(name: &str, config: &Config) -> MeasurementConfig {
287 let has_specific_config = config.get_table(&format!("measurement.{}", name)).is_ok();
289
290 MeasurementConfig {
291 name: name.to_string(),
292 epoch: determine_epoch_from_config(name).map(|e| format!("{:08x}", e)),
293 min_relative_deviation: audit_min_relative_deviation(name),
294 dispersion_method: format!("{:?}", audit_dispersion_method(name)).to_lowercase(),
295 min_measurements: audit_min_measurements(name),
296 aggregate_by: audit_aggregate_by(name).map(|f| format!("{:?}", f).to_lowercase()),
297 sigma: audit_sigma(name),
298 unit: measurement_unit(name),
299 from_parent_fallback: !has_specific_config,
300 }
301}
302
303fn validate_config(measurements: &HashMap<String, MeasurementConfig>) -> Result<Vec<String>> {
305 let mut issues = Vec::new();
306
307 for (name, config) in measurements {
308 if config.epoch.is_none() {
310 issues.push(format!(
311 "Measurement '{}': No epoch configured (run 'git perf bump-epoch -m {}')",
312 name, name
313 ));
314 }
315
316 if let Some(sigma) = config.sigma {
318 if sigma <= 0.0 {
319 issues.push(format!(
320 "Measurement '{}': Invalid sigma value {} (must be positive)",
321 name, sigma
322 ));
323 }
324 }
325
326 if let Some(deviation) = config.min_relative_deviation {
328 if deviation < 0.0 {
329 issues.push(format!(
330 "Measurement '{}': Invalid min_relative_deviation {} (must be non-negative)",
331 name, deviation
332 ));
333 }
334 }
335
336 if let Some(min_meas) = config.min_measurements {
338 if min_meas < 2 {
339 issues.push(format!(
340 "Measurement '{}': Invalid min_measurements {} (must be at least 2)",
341 name, min_meas
342 ));
343 }
344 }
345 }
346
347 Ok(issues)
348}
349
350fn display_human_readable(info: &ConfigInfo, detailed: bool) -> Result<()> {
352 println!("Git-Perf Configuration");
353 println!("======================");
354 println!();
355
356 println!("Git Context:");
358 println!(" Branch: {}", info.git_context.branch);
359 println!(
360 " Repository: {}",
361 info.git_context.repository_root.display()
362 );
363 println!();
364
365 println!("Configuration Sources:");
367 if let Some(ref system_path) = info.config_sources.system_config {
368 println!(" System config: {}", system_path.display());
369 } else {
370 println!(" System config: (none)");
371 }
372 if let Some(ref local_path) = info.config_sources.local_config {
373 println!(" Local config: {}", local_path.display());
374 } else {
375 println!(" Local config: (none)");
376 }
377 println!();
378
379 println!("Global Settings:");
381 println!(
382 " backoff.max_elapsed_seconds: {}",
383 info.global_settings.backoff_max_elapsed_seconds
384 );
385 println!();
386
387 if info.measurements.is_empty() {
389 println!("Measurements: (none configured)");
390 } else {
391 println!("Measurements: ({} configured)", info.measurements.len());
392 println!();
393
394 let mut sorted_measurements: Vec<_> = info.measurements.values().collect();
395 sorted_measurements.sort_by_key(|m| &m.name);
396
397 for measurement in sorted_measurements {
398 display_measurement_human(measurement, detailed);
399 }
400 }
401
402 if let Some(ref issues) = info.validation_issues {
404 if !issues.is_empty() {
405 println!();
406 println!("Validation Issues:");
407 for issue in issues {
408 println!(" \u{26A0} {}", issue);
409 }
410 } else {
411 println!();
412 println!("\u{2713} Configuration is valid");
413 }
414 }
415
416 Ok(())
417}
418
419fn display_measurement_human(measurement: &MeasurementConfig, detailed: bool) {
421 if detailed {
422 println!(" [{}]", measurement.name);
423 if measurement.from_parent_fallback {
424 println!(" (using parent table defaults)");
425 }
426 println!(" epoch: {:?}", measurement.epoch);
427 println!(
428 " min_relative_deviation: {:?}",
429 measurement.min_relative_deviation
430 );
431 println!(
432 " dispersion_method: {}",
433 measurement.dispersion_method
434 );
435 println!(
436 " min_measurements: {:?}",
437 measurement.min_measurements
438 );
439 println!(" aggregate_by: {:?}", measurement.aggregate_by);
440 println!(" sigma: {:?}", measurement.sigma);
441 println!(" unit: {:?}", measurement.unit);
442 println!();
443 } else {
444 let epoch_display = measurement.epoch.as_deref().unwrap_or("(not set)");
446 let unit_display = measurement.unit.as_deref().unwrap_or("(not set)");
447 println!(
448 " {} - epoch: {}, unit: {}",
449 measurement.name, epoch_display, unit_display
450 );
451 }
452}
453
454fn display_json(info: &ConfigInfo) -> Result<()> {
456 let json =
457 serde_json::to_string_pretty(info).context("Failed to serialize configuration to JSON")?;
458 println!("{}", json);
459 Ok(())
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::test_helpers::{
466 dir_with_repo, hermetic_git_env, with_isolated_home, write_gitperfconfig, DirGuard,
467 };
468 use std::env;
469 use std::fs;
470 use std::path::Path;
471
472 #[test]
473 fn test_gather_git_context() {
474 hermetic_git_env();
475 with_isolated_home(|home_path| {
476 let temp_dir = dir_with_repo();
477 let _guard = DirGuard::new(temp_dir.path());
478 env::set_var("HOME", home_path);
479
480 let context = gather_git_context().unwrap();
481 assert_eq!(context.branch, "master");
482 assert!(context.repository_root.exists());
483 });
484 }
485
486 #[test]
487 fn test_find_system_config_xdg() {
488 hermetic_git_env();
489 with_isolated_home(|home_path| {
490 let xdg_config_dir = Path::new(home_path).join("xdg_config");
492 env::set_var("XDG_CONFIG_HOME", &xdg_config_dir);
493
494 let system_config_dir = xdg_config_dir.join("git-perf");
496 fs::create_dir_all(&system_config_dir).unwrap();
497 let system_config_path = system_config_dir.join("config.toml");
498 fs::write(&system_config_path, "# test config\n").unwrap();
499
500 let result = find_system_config();
501 assert_eq!(result, Some(system_config_path));
502 });
503 }
504
505 #[test]
506 fn test_find_system_config_home_fallback() {
507 hermetic_git_env();
508 with_isolated_home(|home_path| {
509 let config_dir = Path::new(home_path).join(".config").join("git-perf");
511 fs::create_dir_all(&config_dir).unwrap();
512 let config_path = config_dir.join("config.toml");
513 fs::write(&config_path, "# test config\n").unwrap();
514
515 let result = find_system_config();
516 assert_eq!(result, Some(config_path));
517 });
518 }
519
520 #[test]
521 fn test_find_system_config_none() {
522 hermetic_git_env();
523 with_isolated_home(|_home_path| {
524 let result = find_system_config();
525 assert_eq!(result, None);
526 });
527 }
528
529 #[test]
530 fn test_get_local_config_path_exists() {
531 hermetic_git_env();
532 with_isolated_home(|home_path| {
533 let temp_dir = dir_with_repo();
534 let _guard = DirGuard::new(temp_dir.path());
535 env::set_var("HOME", home_path);
536
537 write_gitperfconfig(temp_dir.path(), "[measurement]\n");
538
539 let result = get_local_config_path();
540 assert_eq!(result, Some(temp_dir.path().join(".gitperfconfig")));
541 });
542 }
543
544 #[test]
545 fn test_get_local_config_path_none() {
546 hermetic_git_env();
547 with_isolated_home(|home_path| {
548 let temp_dir = dir_with_repo();
549 let _guard = DirGuard::new(temp_dir.path());
550 env::set_var("HOME", home_path);
551
552 let result = get_local_config_path();
553 assert_eq!(result, None);
554 });
555 }
556
557 #[test]
558 fn test_gather_config_sources() {
559 hermetic_git_env();
560 with_isolated_home(|home_path| {
561 let temp_dir = dir_with_repo();
562 let _guard = DirGuard::new(temp_dir.path());
563 env::set_var("HOME", home_path);
564
565 let system_config_dir = Path::new(home_path).join(".config").join("git-perf");
567 fs::create_dir_all(&system_config_dir).unwrap();
568 let system_config_path = system_config_dir.join("config.toml");
569 fs::write(&system_config_path, "# system config\n").unwrap();
570
571 write_gitperfconfig(temp_dir.path(), "[measurement]\n");
572
573 let sources = gather_config_sources().unwrap();
574 assert_eq!(sources.system_config, Some(system_config_path));
575 assert_eq!(
576 sources.local_config,
577 Some(temp_dir.path().join(".gitperfconfig"))
578 );
579 });
580 }
581
582 #[test]
583 fn test_gather_global_settings() {
584 hermetic_git_env();
585 with_isolated_home(|_home_path| {
586 let settings = gather_global_settings();
587 assert_eq!(settings.backoff_max_elapsed_seconds, 60);
589 });
590 }
591
592 #[test]
593 fn test_extract_measurement_names_empty() {
594 hermetic_git_env();
595 with_isolated_home(|home_path| {
596 let temp_dir = dir_with_repo();
597 let _guard = DirGuard::new(temp_dir.path());
598 env::set_var("HOME", home_path);
599
600 let config = Config::builder().build().unwrap();
601 let names = extract_measurement_names(&config).unwrap();
602 assert!(names.is_empty());
603 });
604 }
605
606 #[test]
607 fn test_extract_measurement_names_with_measurements() {
608 hermetic_git_env();
609 with_isolated_home(|home_path| {
610 let temp_dir = dir_with_repo();
611 let _guard = DirGuard::new(temp_dir.path());
612 env::set_var("HOME", home_path);
613
614 write_gitperfconfig(
615 temp_dir.path(),
616 r#"
617[measurement.build_time]
618epoch = 0x12345678
619
620[measurement.test_time]
621epoch = 0x87654321
622"#,
623 );
624
625 let config = read_hierarchical_config().unwrap();
626 let mut names = extract_measurement_names(&config).unwrap();
627 names.sort(); assert_eq!(names, vec!["build_time", "test_time"]);
630 });
631 }
632
633 #[test]
634 fn test_gather_single_measurement_config() {
635 hermetic_git_env();
636 with_isolated_home(|home_path| {
637 let temp_dir = dir_with_repo();
638 let _guard = DirGuard::new(temp_dir.path());
639 env::set_var("HOME", home_path);
640
641 write_gitperfconfig(
642 temp_dir.path(),
643 r#"
644[measurement.build_time]
645epoch = "12345678"
646min_relative_deviation = 5.0
647dispersion_method = "mad"
648min_measurements = 10
649aggregate_by = "median"
650sigma = 2.0
651unit = "ms"
652"#,
653 );
654
655 let config = read_hierarchical_config().unwrap();
656 let meas_config = gather_single_measurement_config("build_time", &config);
657
658 assert_eq!(meas_config.name, "build_time");
659 assert_eq!(meas_config.epoch, Some("12345678".to_string()));
660 assert_eq!(meas_config.min_relative_deviation, Some(5.0));
661 assert_eq!(meas_config.dispersion_method, "medianabsolutedeviation");
662 assert_eq!(meas_config.min_measurements, Some(10));
663 assert_eq!(meas_config.aggregate_by, Some("median".to_string()));
664 assert_eq!(meas_config.sigma, Some(2.0));
665 assert_eq!(meas_config.unit, Some("ms".to_string()));
666 assert!(!meas_config.from_parent_fallback);
667 });
668 }
669
670 #[test]
671 fn test_gather_single_measurement_config_parent_fallback() {
672 hermetic_git_env();
673 with_isolated_home(|home_path| {
674 let temp_dir = dir_with_repo();
675 let _guard = DirGuard::new(temp_dir.path());
676 env::set_var("HOME", home_path);
677
678 write_gitperfconfig(
679 temp_dir.path(),
680 r#"
681[measurement]
682dispersion_method = "stddev"
683"#,
684 );
685
686 let config = read_hierarchical_config().unwrap();
687 let meas_config = gather_single_measurement_config("build_time", &config);
688
689 assert_eq!(meas_config.name, "build_time");
690 assert_eq!(meas_config.dispersion_method, "standarddeviation");
691 assert!(meas_config.from_parent_fallback);
692 });
693 }
694
695 #[test]
696 fn test_validate_config_valid() {
697 let mut measurements = HashMap::new();
698 measurements.insert(
699 "build_time".to_string(),
700 MeasurementConfig {
701 name: "build_time".to_string(),
702 epoch: Some("12345678".to_string()),
703 min_relative_deviation: Some(5.0),
704 dispersion_method: "stddev".to_string(),
705 min_measurements: Some(10),
706 aggregate_by: Some("mean".to_string()),
707 sigma: Some(3.0),
708 unit: Some("ms".to_string()),
709 from_parent_fallback: false,
710 },
711 );
712
713 let issues = validate_config(&measurements).unwrap();
714 assert!(issues.is_empty());
715 }
716
717 #[test]
718 fn test_validate_config_missing_epoch() {
719 let mut measurements = HashMap::new();
720 measurements.insert(
721 "build_time".to_string(),
722 MeasurementConfig {
723 name: "build_time".to_string(),
724 epoch: None,
725 min_relative_deviation: Some(5.0),
726 dispersion_method: "stddev".to_string(),
727 min_measurements: Some(10),
728 aggregate_by: Some("mean".to_string()),
729 sigma: Some(3.0),
730 unit: Some("ms".to_string()),
731 from_parent_fallback: false,
732 },
733 );
734
735 let issues = validate_config(&measurements).unwrap();
736 assert_eq!(issues.len(), 1);
737 assert!(issues[0].contains("No epoch configured"));
738 }
739
740 #[test]
741 fn test_validate_config_invalid_sigma() {
742 let mut measurements = HashMap::new();
743 measurements.insert(
744 "build_time".to_string(),
745 MeasurementConfig {
746 name: "build_time".to_string(),
747 epoch: Some("12345678".to_string()),
748 min_relative_deviation: Some(5.0),
749 dispersion_method: "stddev".to_string(),
750 min_measurements: Some(10),
751 aggregate_by: Some("mean".to_string()),
752 sigma: Some(-1.0),
753 unit: Some("ms".to_string()),
754 from_parent_fallback: false,
755 },
756 );
757
758 let issues = validate_config(&measurements).unwrap();
759 assert_eq!(issues.len(), 1);
760 assert!(issues[0].contains("Invalid sigma value"));
761 }
762
763 #[test]
764 fn test_validate_config_invalid_min_relative_deviation() {
765 let mut measurements = HashMap::new();
766 measurements.insert(
767 "build_time".to_string(),
768 MeasurementConfig {
769 name: "build_time".to_string(),
770 epoch: Some("12345678".to_string()),
771 min_relative_deviation: Some(-5.0),
772 dispersion_method: "stddev".to_string(),
773 min_measurements: Some(10),
774 aggregate_by: Some("mean".to_string()),
775 sigma: Some(3.0),
776 unit: Some("ms".to_string()),
777 from_parent_fallback: false,
778 },
779 );
780
781 let issues = validate_config(&measurements).unwrap();
782 assert_eq!(issues.len(), 1);
783 assert!(issues[0].contains("Invalid min_relative_deviation"));
784 }
785
786 #[test]
787 fn test_validate_config_invalid_min_measurements() {
788 let mut measurements = HashMap::new();
789 measurements.insert(
790 "build_time".to_string(),
791 MeasurementConfig {
792 name: "build_time".to_string(),
793 epoch: Some("12345678".to_string()),
794 min_relative_deviation: Some(5.0),
795 dispersion_method: "stddev".to_string(),
796 min_measurements: Some(1),
797 aggregate_by: Some("mean".to_string()),
798 sigma: Some(3.0),
799 unit: Some("ms".to_string()),
800 from_parent_fallback: false,
801 },
802 );
803
804 let issues = validate_config(&measurements).unwrap();
805 assert_eq!(issues.len(), 1);
806 assert!(issues[0].contains("Invalid min_measurements"));
807 }
808
809 #[test]
810 fn test_validate_config_multiple_issues() {
811 let mut measurements = HashMap::new();
812 measurements.insert(
813 "build_time".to_string(),
814 MeasurementConfig {
815 name: "build_time".to_string(),
816 epoch: None,
817 min_relative_deviation: Some(-5.0),
818 dispersion_method: "stddev".to_string(),
819 min_measurements: Some(1),
820 aggregate_by: Some("mean".to_string()),
821 sigma: Some(-3.0),
822 unit: Some("ms".to_string()),
823 from_parent_fallback: false,
824 },
825 );
826
827 let issues = validate_config(&measurements).unwrap();
828 assert_eq!(issues.len(), 4); }
830
831 #[test]
832 fn test_gather_measurement_configs_empty() {
833 hermetic_git_env();
834 with_isolated_home(|home_path| {
835 let temp_dir = dir_with_repo();
836 let _guard = DirGuard::new(temp_dir.path());
837 env::set_var("HOME", home_path);
838
839 let measurements = gather_measurement_configs(None).unwrap();
841 assert!(measurements.is_empty());
842 });
843 }
844
845 #[test]
846 fn test_gather_measurement_configs_with_filter() {
847 hermetic_git_env();
848 with_isolated_home(|home_path| {
849 let temp_dir = dir_with_repo();
850 let _guard = DirGuard::new(temp_dir.path());
851 env::set_var("HOME", home_path);
852
853 write_gitperfconfig(
854 temp_dir.path(),
855 r#"
856[measurement.build_time]
857epoch = 0x12345678
858
859[measurement.test_time]
860epoch = 0x87654321
861"#,
862 );
863
864 let measurements = gather_measurement_configs(Some("build_time")).unwrap();
865 assert_eq!(measurements.len(), 1);
866 assert!(measurements.contains_key("build_time"));
867 assert!(!measurements.contains_key("test_time"));
868 });
869 }
870
871 #[test]
872 fn test_config_info_serialization() {
873 hermetic_git_env();
874 with_isolated_home(|home_path| {
875 let config_info = ConfigInfo {
876 git_context: GitContext {
877 branch: "master".to_string(),
878 repository_root: PathBuf::from(home_path),
879 },
880 config_sources: ConfigSources {
881 system_config: None,
882 local_config: Some(PathBuf::from(home_path).join(".gitperfconfig")),
883 },
884 global_settings: GlobalSettings {
885 backoff_max_elapsed_seconds: 60,
886 },
887 measurements: HashMap::new(),
888 validation_issues: None,
889 };
890
891 let json = serde_json::to_string_pretty(&config_info).unwrap();
893 assert!(json.contains("master"));
894 assert!(json.contains("backoff_max_elapsed_seconds"));
895
896 let deserialized: ConfigInfo = serde_json::from_str(&json).unwrap();
898 assert_eq!(deserialized.git_context.branch, "master");
899 });
900 }
901
902 #[test]
903 fn test_display_measurement_human_detailed() {
904 let measurement = MeasurementConfig {
905 name: "build_time".to_string(),
906 epoch: Some("12345678".to_string()),
907 min_relative_deviation: Some(5.0),
908 dispersion_method: "stddev".to_string(),
909 min_measurements: Some(10),
910 aggregate_by: Some("mean".to_string()),
911 sigma: Some(3.0),
912 unit: Some("ms".to_string()),
913 from_parent_fallback: false,
914 };
915
916 display_measurement_human(&measurement, true);
918 }
919
920 #[test]
921 fn test_display_measurement_human_summary() {
922 let measurement = MeasurementConfig {
923 name: "build_time".to_string(),
924 epoch: Some("12345678".to_string()),
925 min_relative_deviation: Some(5.0),
926 dispersion_method: "stddev".to_string(),
927 min_measurements: Some(10),
928 aggregate_by: Some("mean".to_string()),
929 sigma: Some(3.0),
930 unit: Some("ms".to_string()),
931 from_parent_fallback: false,
932 };
933
934 display_measurement_human(&measurement, false);
936 }
937}