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::{with_isolated_test_setup, write_gitperfconfig};
466 use std::env;
467 use std::fs;
468 use std::path::Path;
469
470 #[test]
471 fn test_gather_git_context() {
472 with_isolated_test_setup(|_git_dir, _home_path| {
473 let context = gather_git_context().unwrap();
474 assert_eq!(context.branch, "master");
475 assert!(context.repository_root.exists());
476 });
477 }
478
479 #[test]
480 fn test_find_system_config_xdg() {
481 with_isolated_test_setup(|_git_dir, home_path| {
482 let xdg_config_dir = Path::new(home_path).join("xdg_config");
484 env::set_var("XDG_CONFIG_HOME", &xdg_config_dir);
485
486 let system_config_dir = xdg_config_dir.join("git-perf");
488 fs::create_dir_all(&system_config_dir).unwrap();
489 let system_config_path = system_config_dir.join("config.toml");
490 fs::write(&system_config_path, "# test config\n").unwrap();
491
492 let result = find_system_config();
493 assert_eq!(result, Some(system_config_path));
494 });
495 }
496
497 #[test]
498 fn test_find_system_config_home_fallback() {
499 with_isolated_test_setup(|_git_dir, home_path| {
500 let config_dir = Path::new(home_path).join(".config").join("git-perf");
502 fs::create_dir_all(&config_dir).unwrap();
503 let config_path = config_dir.join("config.toml");
504 fs::write(&config_path, "# test config\n").unwrap();
505
506 let result = find_system_config();
507 assert_eq!(result, Some(config_path));
508 });
509 }
510
511 #[test]
512 fn test_find_system_config_none() {
513 with_isolated_test_setup(|_git_dir, _home_path| {
514 let result = find_system_config();
515 assert_eq!(result, None);
516 });
517 }
518
519 #[test]
520 fn test_get_local_config_path_exists() {
521 with_isolated_test_setup(|git_dir, _home_path| {
522 write_gitperfconfig(git_dir, "[measurement]\n");
523
524 let result = get_local_config_path();
525 assert_eq!(
527 result.map(|p| p.canonicalize().unwrap()),
528 Some(git_dir.join(".gitperfconfig").canonicalize().unwrap())
529 );
530 });
531 }
532
533 #[test]
534 fn test_get_local_config_path_none() {
535 with_isolated_test_setup(|_git_dir, _home_path| {
536 let result = get_local_config_path();
537 assert_eq!(result, None);
538 });
539 }
540
541 #[test]
542 fn test_gather_config_sources() {
543 with_isolated_test_setup(|git_dir, home_path| {
544 let system_config_dir = Path::new(home_path).join(".config").join("git-perf");
546 fs::create_dir_all(&system_config_dir).unwrap();
547 let system_config_path = system_config_dir.join("config.toml");
548 fs::write(&system_config_path, "# system config\n").unwrap();
549
550 write_gitperfconfig(git_dir, "[measurement]\n");
551
552 let sources = gather_config_sources().unwrap();
553 assert_eq!(sources.system_config, Some(system_config_path));
554 assert_eq!(
556 sources.local_config.map(|p| p.canonicalize().unwrap()),
557 Some(git_dir.join(".gitperfconfig").canonicalize().unwrap())
558 );
559 });
560 }
561
562 #[test]
563 fn test_gather_global_settings() {
564 with_isolated_test_setup(|_git_dir, _home_path| {
565 let settings = gather_global_settings();
566 assert_eq!(settings.backoff_max_elapsed_seconds, 60);
568 });
569 }
570
571 #[test]
572 fn test_extract_measurement_names_empty() {
573 with_isolated_test_setup(|_git_dir, _home_path| {
574 let config = Config::builder().build().unwrap();
575 let names = extract_measurement_names(&config).unwrap();
576 assert!(names.is_empty());
577 });
578 }
579
580 #[test]
581 fn test_extract_measurement_names_with_measurements() {
582 with_isolated_test_setup(|git_dir, _home_path| {
583 write_gitperfconfig(
584 git_dir,
585 r#"
586[measurement.build_time]
587epoch = 0x12345678
588
589[measurement.test_time]
590epoch = 0x87654321
591"#,
592 );
593
594 let config = read_hierarchical_config().unwrap();
595 let mut names = extract_measurement_names(&config).unwrap();
596 names.sort(); assert_eq!(names, vec!["build_time", "test_time"]);
599 });
600 }
601
602 #[test]
603 fn test_gather_single_measurement_config() {
604 with_isolated_test_setup(|git_dir, _home_path| {
605 write_gitperfconfig(
606 git_dir,
607 r#"
608[measurement.build_time]
609epoch = "12345678"
610min_relative_deviation = 5.0
611dispersion_method = "mad"
612min_measurements = 10
613aggregate_by = "median"
614sigma = 2.0
615unit = "ms"
616"#,
617 );
618
619 let config = read_hierarchical_config().unwrap();
620 let meas_config = gather_single_measurement_config("build_time", &config);
621
622 assert_eq!(meas_config.name, "build_time");
623 assert_eq!(meas_config.epoch, Some("12345678".to_string()));
624 assert_eq!(meas_config.min_relative_deviation, Some(5.0));
625 assert_eq!(meas_config.dispersion_method, "medianabsolutedeviation");
626 assert_eq!(meas_config.min_measurements, Some(10));
627 assert_eq!(meas_config.aggregate_by, Some("median".to_string()));
628 assert_eq!(meas_config.sigma, Some(2.0));
629 assert_eq!(meas_config.unit, Some("ms".to_string()));
630 assert!(!meas_config.from_parent_fallback);
631 });
632 }
633
634 #[test]
635 fn test_gather_single_measurement_config_parent_fallback() {
636 with_isolated_test_setup(|git_dir, _home_path| {
637 write_gitperfconfig(
638 git_dir,
639 r#"
640[measurement]
641dispersion_method = "stddev"
642"#,
643 );
644
645 let config = read_hierarchical_config().unwrap();
646 let meas_config = gather_single_measurement_config("build_time", &config);
647
648 assert_eq!(meas_config.name, "build_time");
649 assert_eq!(meas_config.dispersion_method, "standarddeviation");
650 assert!(meas_config.from_parent_fallback);
651 });
652 }
653
654 #[test]
655 fn test_validate_config_valid() {
656 let mut measurements = HashMap::new();
657 measurements.insert(
658 "build_time".to_string(),
659 MeasurementConfig {
660 name: "build_time".to_string(),
661 epoch: Some("12345678".to_string()),
662 min_relative_deviation: Some(5.0),
663 dispersion_method: "stddev".to_string(),
664 min_measurements: Some(10),
665 aggregate_by: Some("mean".to_string()),
666 sigma: Some(3.0),
667 unit: Some("ms".to_string()),
668 from_parent_fallback: false,
669 },
670 );
671
672 let issues = validate_config(&measurements).unwrap();
673 assert!(issues.is_empty());
674 }
675
676 #[test]
677 fn test_validate_config_missing_epoch() {
678 let mut measurements = HashMap::new();
679 measurements.insert(
680 "build_time".to_string(),
681 MeasurementConfig {
682 name: "build_time".to_string(),
683 epoch: None,
684 min_relative_deviation: Some(5.0),
685 dispersion_method: "stddev".to_string(),
686 min_measurements: Some(10),
687 aggregate_by: Some("mean".to_string()),
688 sigma: Some(3.0),
689 unit: Some("ms".to_string()),
690 from_parent_fallback: false,
691 },
692 );
693
694 let issues = validate_config(&measurements).unwrap();
695 assert_eq!(issues.len(), 1);
696 assert!(issues[0].contains("No epoch configured"));
697 }
698
699 #[test]
700 fn test_validate_config_invalid_sigma() {
701 let mut measurements = HashMap::new();
702 measurements.insert(
703 "build_time".to_string(),
704 MeasurementConfig {
705 name: "build_time".to_string(),
706 epoch: Some("12345678".to_string()),
707 min_relative_deviation: Some(5.0),
708 dispersion_method: "stddev".to_string(),
709 min_measurements: Some(10),
710 aggregate_by: Some("mean".to_string()),
711 sigma: Some(-1.0),
712 unit: Some("ms".to_string()),
713 from_parent_fallback: false,
714 },
715 );
716
717 let issues = validate_config(&measurements).unwrap();
718 assert_eq!(issues.len(), 1);
719 assert!(issues[0].contains("Invalid sigma value"));
720 }
721
722 #[test]
723 fn test_validate_config_invalid_min_relative_deviation() {
724 let mut measurements = HashMap::new();
725 measurements.insert(
726 "build_time".to_string(),
727 MeasurementConfig {
728 name: "build_time".to_string(),
729 epoch: Some("12345678".to_string()),
730 min_relative_deviation: Some(-5.0),
731 dispersion_method: "stddev".to_string(),
732 min_measurements: Some(10),
733 aggregate_by: Some("mean".to_string()),
734 sigma: Some(3.0),
735 unit: Some("ms".to_string()),
736 from_parent_fallback: false,
737 },
738 );
739
740 let issues = validate_config(&measurements).unwrap();
741 assert_eq!(issues.len(), 1);
742 assert!(issues[0].contains("Invalid min_relative_deviation"));
743 }
744
745 #[test]
746 fn test_validate_config_invalid_min_measurements() {
747 let mut measurements = HashMap::new();
748 measurements.insert(
749 "build_time".to_string(),
750 MeasurementConfig {
751 name: "build_time".to_string(),
752 epoch: Some("12345678".to_string()),
753 min_relative_deviation: Some(5.0),
754 dispersion_method: "stddev".to_string(),
755 min_measurements: Some(1),
756 aggregate_by: Some("mean".to_string()),
757 sigma: Some(3.0),
758 unit: Some("ms".to_string()),
759 from_parent_fallback: false,
760 },
761 );
762
763 let issues = validate_config(&measurements).unwrap();
764 assert_eq!(issues.len(), 1);
765 assert!(issues[0].contains("Invalid min_measurements"));
766 }
767
768 #[test]
769 fn test_validate_config_multiple_issues() {
770 let mut measurements = HashMap::new();
771 measurements.insert(
772 "build_time".to_string(),
773 MeasurementConfig {
774 name: "build_time".to_string(),
775 epoch: None,
776 min_relative_deviation: Some(-5.0),
777 dispersion_method: "stddev".to_string(),
778 min_measurements: Some(1),
779 aggregate_by: Some("mean".to_string()),
780 sigma: Some(-3.0),
781 unit: Some("ms".to_string()),
782 from_parent_fallback: false,
783 },
784 );
785
786 let issues = validate_config(&measurements).unwrap();
787 assert_eq!(issues.len(), 4); }
789
790 #[test]
791 fn test_gather_measurement_configs_empty() {
792 with_isolated_test_setup(|_git_dir, _home_path| {
793 let measurements = gather_measurement_configs(None).unwrap();
795 assert!(measurements.is_empty());
796 });
797 }
798
799 #[test]
800 fn test_gather_measurement_configs_with_filter() {
801 with_isolated_test_setup(|git_dir, _home_path| {
802 write_gitperfconfig(
803 git_dir,
804 r#"
805[measurement.build_time]
806epoch = 0x12345678
807
808[measurement.test_time]
809epoch = 0x87654321
810"#,
811 );
812
813 let measurements = gather_measurement_configs(Some("build_time")).unwrap();
814 assert_eq!(measurements.len(), 1);
815 assert!(measurements.contains_key("build_time"));
816 assert!(!measurements.contains_key("test_time"));
817 });
818 }
819
820 #[test]
821 fn test_config_info_serialization() {
822 with_isolated_test_setup(|_git_dir, home_path| {
823 let config_info = ConfigInfo {
824 git_context: GitContext {
825 branch: "master".to_string(),
826 repository_root: PathBuf::from(home_path),
827 },
828 config_sources: ConfigSources {
829 system_config: None,
830 local_config: Some(PathBuf::from(home_path).join(".gitperfconfig")),
831 },
832 global_settings: GlobalSettings {
833 backoff_max_elapsed_seconds: 60,
834 },
835 measurements: HashMap::new(),
836 validation_issues: None,
837 };
838
839 let json = serde_json::to_string_pretty(&config_info).unwrap();
841 assert!(json.contains("master"));
842 assert!(json.contains("backoff_max_elapsed_seconds"));
843
844 let deserialized: ConfigInfo = serde_json::from_str(&json).unwrap();
846 assert_eq!(deserialized.git_context.branch, "master");
847 });
848 }
849
850 #[test]
851 fn test_display_measurement_human_detailed() {
852 let measurement = MeasurementConfig {
853 name: "build_time".to_string(),
854 epoch: Some("12345678".to_string()),
855 min_relative_deviation: Some(5.0),
856 dispersion_method: "stddev".to_string(),
857 min_measurements: Some(10),
858 aggregate_by: Some("mean".to_string()),
859 sigma: Some(3.0),
860 unit: Some("ms".to_string()),
861 from_parent_fallback: false,
862 };
863
864 display_measurement_human(&measurement, true);
866 }
867
868 #[test]
869 fn test_display_measurement_human_summary() {
870 let measurement = MeasurementConfig {
871 name: "build_time".to_string(),
872 epoch: Some("12345678".to_string()),
873 min_relative_deviation: Some(5.0),
874 dispersion_method: "stddev".to_string(),
875 min_measurements: Some(10),
876 aggregate_by: Some("mean".to_string()),
877 sigma: Some(3.0),
878 unit: Some("ms".to_string()),
879 from_parent_fallback: false,
880 };
881
882 display_measurement_human(&measurement, false);
884 }
885}